Three.js 模型反射效果教程

📅 2026/7/3 1:44:32
Three.js 模型反射效果教程
模型反射效果 ·Model Blend· ▶ 在线运行案例案例合集三维可视化功能案例threehub.cn开源仓库github地址https://github.com/z2586300277/three-cesium-examples400个案例代码:网盘链接你将学到什么ShaderMaterial 自定义着色器实现核心视觉效果onBeforeCompile 注入 GLSL 改造内置材质OrbitControls 相机轨道交互glTF/Draco 模型加载与优化水面反射/镜像材质requestAnimationFrame渲染循环与resize自适应效果说明本案例演示模型反射效果效果基于 WebGL 实现「模型反射效果」可视化效果附完整可运行源码核心用到 ShaderMaterial、onBeforeCompile、OrbitControls。建议先打开文首在线案例查看动态画面再对照下方源码逐步理解。核心概念Scene / Camera / WebGLRenderer构成最小渲染闭环大场景可开logarithmicDepthBuffer缓解 Z-fighting。ShaderMaterial通过uniforms 自定义 GLSL 控制逐像素/逐点效果透明粒子常配合depthTest: false。onBeforeCompile在 Three 拼好内置 shader 后替换#include片段适合在 PBR 材质上叠加大屏特效。OrbitControls提供轨道旋转/缩放开启enableDamping后需在 animate 中controls.update()。实现步骤搭建 Scene、PerspectiveCamera、WebGLRenderer挂载 canvas 并处理resize异步加载模型 / 3D Tiles / GeoJSON 等资源并加入 scene 或 entities定义 uniforms / onBeforeCompile 或 ShaderMaterial编写 GLSL 与材质参数创建 OrbitControls及 Raycaster 等交互控件若源码包含在requestAnimationFrame循环中更新状态并 renderCesium 为viewer.render或自动渲染代码要点import * as THREE from threeimport { OrbitControls } from three/examples/jsm/controls/OrbitControls.js import { GLTFLoader } from three/examples/jsm/loaders/GLTFLoader.js import { DRACOLoader } from three/examples/jsm/loaders/DRACOLoader.jsconst box document.getElementById(box)const scene new THREE.Scene()const camera new THREE.PerspectiveCamera(75, box.clientWidth / box.clientHeight, 0.1, 1000)camera.position.set(2, 1, 3)const renderer new THREE.WebGLRenderer({ antialias: true, alpha: true, logarithmicDepthBuffer: true })renderer.setSize(box.clientWidth, box.clientHeight)box.appendChild(renderer.domElement)const controls new OrbitControls(camera, renderer.domElement)new GLTFLoader().setDRACOLoader(new DRACOLoader().setDecoderPath(FILE_HOST js/three/draco/)).load(HOST /files/model/car.glb, gltf scene.add(gltf.scene))const pointLight new THREE.PointLight(0xffffff, 0.5, 0, 0)pointLight.position.set(0, 10, 0)scene.add(pointLight)const AmbientLight new THREE.AmbientLight(0xffffff, 0.5)scene.add(AmbientLight)animate()function animate() {requestAnimationFrame(animate)controls.update()renderer.render(scene, camera)}window.onresize () {renderer.setSize(box.clientWidth, box.clientHeight)camera.aspect box.clientWidth / box.clientHeightcamera.updateProjectionMatrix()}import { Color, Matrix4, Mesh, PerspectiveCamera, Plane, ShaderMaterial, UniformsUtils, Vector3, Vector4, WebGLRenderTarget, HalfFloatType, } from three;class Reflector extends Mesh { constructor(geometry, options {}) { super(geometry);this.isReflector true;this.type Reflector; this.camera new PerspectiveCamera();const scope this;const color options.color ! undefined ? new Color(options.color) : new Color(0x7f7f7f); const textureWidth options.textureWidth || 512; const textureHeight options.textureHeight || 512; const clipBias options.clipBias || 0; const shader options.shader || Reflector.ReflectorShader; const multisample options.multisample ! undefined ? options.multisample : 4;//const reflectorPlane new Plane(); const normal new Vector3(); const reflectorWorldPosition new Vector3(); const cameraWorldPosition new Vector3(); const rotationMatrix new Matrix4(); const lookAtPosition new Vector3(0, 0, -1); const clipPlane new Vector4();const view new Vector3(); const target new Vector3(); const q new Vector4();const textureMatrix new Matrix4(); const virtualCamera this.camera;const renderTarget new WebGLRenderTarget( textureWidth, textureHeight, { samples: multisample, type: HalfFloatType } );const material new ShaderMaterial({ name: shader.name ! undefined ? shader.name : unspecified, uniforms: UniformsUtils.clone(shader.uniforms), fragmentShader: shader.fragmentShader, vertexShader: shader.vertexShader, transparent: true, });material.uniforms[tDiffuse].value renderTarget.texture; material.uniforms[_opacity].value options.opacity || 1; material.uniforms[color].value color; material.uniforms[textureMatrix].value textureMatrix;this.material material; this.count 0; this.onBeforeRender (renderer, scene, camera) { this.count;// if (this.count % 4 0) { // return; // }reflectorWorldPosition.setFromMatrixPosition(scope.matrixWorld); cameraWorldPosition.setFromMatrixPosition(camera.matrixWorld);rotationMatrix.extractRotation(scope.matrixWorld);normal.set(0, 0, 1); normal.applyMatrix4(rotationMatrix);view.subVectors(reflectorWorldPosition, cameraWorldPosition);// Avoid rendering when reflector is facing awayif (view.dot(normal) 0) return;view.reflect(normal).negate(); view.add(reflectorWorldPosition);rotationMatrix.extractRotation(camera.matrixWorld);lookAtPosition.set(0, 0, -1); lookAtPosition.applyMatrix4(rotationMatrix); lookAtPosition.add(cameraWorldPosition);target.subVectors(reflectorWorldPosition, lookAtPosition); target.reflect(normal).negate(); target.add(reflectorWorldPosition);virtualCamera.position.copy(view); virtualCamera.up.set(0, 1, 0); virtualCamera.up.applyMatrix4(rotationMatrix); virtualCamera.up.reflect(normal); virtualCamera.lookAt(target);virtualCamera.far camera.far; // Used in WebGLBackgroundvirtualCamera.updateMatrixWorld(); virtualCamera.projectionMatrix.copy(camera.projectionMatrix);// Update the texture matrix textureMatrix.set( 0.5, 0.0, 0.0, 0.5, 0.0, 0.5, 0.0, 0.5, 0.0, 0.0, 0.5, 0.5, 0.0, 0.0, 0.0, 1.0 ); textureMatrix.multiply(virtualCamera.projectionMatrix); textureMatrix.multiply(virtualCamera.matrixWorldInverse); textureMatrix.multiply(scope.matrixWorld);// Now update projection matrix with new clip plane, implementing code from: http://www.terathon.com/code/oblique.html // Paper explaining this technique: http://www.terathon.com/lengyel/Lengyel-Oblique.pdf reflectorPlane.setFromNormalAndCoplanarPoint( normal, reflectorWorldPosition ); reflectorPlane.applyMatrix4(virtualCamera.matrixWorldInverse);clipPlane.set( reflectorPlane.normal.x, reflectorPlane.normal.y, reflectorPlane.normal.z, reflectorPlane.constant );const projectionMatrix virtualCamera.projectionMatrix;q.x (Math.sign(clipPlane.x) projectionMatrix.elements[8]) / projectionMatrix.elements[0]; q.y (Math.sign(clipPlane.y) projectionMatrix.elements[9]) / projectionMatrix.elements[5]; q.z -1.0; q.w (1.0 projectionMatrix.elements[10]) / projectionMatrix.elements[14];// Calculate the scaled plane vector clipPlane.multiplyScalar(2.0 / clipPlane.dot(q));// Replacing the third row of the projection matrix projectionMatrix.elements[2] clipPlane.x; projectionMatrix.elements[6] clipPlane.y; projectionMatrix.elements[10] clipPlane.z 1.0 - clipBias; projectionMatrix.elements[14] clipPlane.w;// Render scope.visible false;const currentRenderTarget renderer.getRenderTarget();const currentXrEnabled renderer.xr.enabled; const currentShadowAutoUpdate renderer.shadowMap.autoUpdate;renderer.xr.enabled false; // Avoid camera modification renderer.shadowMap.autoUpdate false; // Avoid re-computing shadowsrenderer.setRenderTarget(renderTarget);renderer.state.buffers.depth.setMask(true); // make sure the depth buffer is writable so it can be properly cleared, see #18897if (renderer.autoClear false) renderer.clear();// filteroptions.filter.forEach((name) { const mesh scene.getObjectByName(name); mesh.visible false; });renderer.render(scene, virtualCamera);options.filter.forEach((name) { const mesh scene.getObjectByName(name); mesh.visible true; });renderer.xr.enabled currentXrEnabled; renderer.shadowMap.autoUpdate currentShadowAutoUpdate;renderer.setRenderTarget(currentRenderTarget);// Restore viewportconst viewport camera.viewport;if (viewport ! undefined) { renderer.state.viewport(viewport); }scope.visible true; };this.getRenderTarget function () { return renderTarget; };this.dispose function () { renderTarget.dispose(); scope.material.dispose(); }; } }Reflector.ReflectorShader { name: ReflectorShader,uniforms: { color: { value: null, },tDiffuse: { value: null, },textureMatrix: { value: null, }, _opacity: { value: null, }, },vertexShader: /glsl/uniform mat4 textureMatrix; varying vec4 vUv;#include #includevoid main() {vUv textureMatrix * vec4( position, 1.0 );gl_Position projectionMatrixmodelViewMatrixvec4( position, 1.0 );#include},fragmentShader: /glsl/uniform vec3 color; uniform float _opacity; uniform sampler2D tDiffuse; varying vec4 vUv;#includefloat blendOverlay( float base, float blend ) {return( base 0.5 ? ( 2.0baseblend ) : ( 1.0 - 2.0( 1.0 - base )( 1.0 - blend ) ) );}vec3 blendOverlay( vec3 base, vec3 blend ) {return vec3( blendOverlay( base.r, blend.r ), blendOverlay( base.g, blend.g ), blendOverlay( base.b, blend.b ) );}void main() {#includevec4 base texture2DProj( tDiffuse, vUv ); gl_FragColor vec4( base.rgb , 0.1 );#include #include}, };/**description: 为mesh的材质增加反光效果param {} meshreturn {} */ function addReflectorEffect(mesh, options { filter: [] }) { const material mesh.material;// material.isReflector true;// material.type Reflector; const camera new PerspectiveCamera();const textureWidth options.textureWidth || 512; const textureHeight options.textureHeight || 512; const clipBias options.clipBias || 0; const shader options.shader || Reflector.ReflectorShader; const multisample options.multisample ! undefined ? options.multisample : 4;const reflectorPlane new Plane(); const normal new Vector3(); const reflectorWorldPosition new Vector3(); const cameraWorldPosition new Vector3(); const rotationMatrix new Matrix4(); const lookAtPosition new Vector3(0, 0, -1); const clipPlane new Vector4();const view new Vector3(); const target new Vector3(); const q new Vector4();const textureMatrix new Matrix4(); const virtualCamera camera;const renderTarget new WebGLRenderTarget(textureWidth, textureHeight, { samples: multisample, type: HalfFloatType, });const appendUniforms { refDiffuse: { value: renderTarget.texture }, // refOpacity: { value: options.opacity || 1 }, refTextureMatrix: { value: textureMatrix }, };material.onBeforeCompile (shader) {Object.assign(shader.uniforms, appendUniforms); shader.vertexShader shader.vertexShader.replace( #include ,#include uniform mat4 refTextureMatrix; varying vec4 refUv;); shader.fragmentShader shader.fragmentShader.replace( #include ,#include uniform sampler2D refDiffuse; varying vec4 refUv;); shader.vertexShader shader.vertexShader.replace( #include ,#include refUv refTextureMatrix * vec4( position, 1.0 );); shader.fragmentShader shader.fragmentShader.replace( #include ,#include gl_FragColor.rgb texture2DProj( refDiffuse, refUv ).rgb; gl_FragColor.a ${options.opacity || 1.0};); // uniform sampler2D refDiffuse; // varying vec4 vUv; // console.log(shader.fragmentShader); };mesh.material.onBeforeRender (renderer, scene, camera) { reflectorWorldPosition.setFromMatrixPosition(mesh.matrixWorld); cameraWorldPosition.setFromMatrixPosition(camera.matrixWorld);rotationMatrix.extractRotation(mesh.matrixWorld);normal.set(0, 0, 1); normal.applyMatrix4(rotationMatrix);view.subVectors(reflectorWorldPosition, cameraWorldPosition);// Avoid rendering when reflector is facing awayif (view.dot(normal) 0) return;view.reflect(normal).negate(); view.add(reflectorWorldPosition);rotationMatrix.extractRotation(camera.matrixWorld);lookAtPosition.set(0, 0, -1); lookAtPosition.applyMatrix4(rotationMatrix); lookAtPosition.add(cameraWorldPosition);target.subVectors(reflectorWorldPosition, lookAtPosition); target.reflect(normal).negate(); target.add(reflectorWorldPosition);virtualCamera.position.copy(view); virtualCamera.up.set(0, 1, 0); virtualCamera.up.applyMatrix4(rotationMatrix); virtualCamera.up.reflect(normal); virtualCamera.lookAt(target);virtualCamera.far camera.far; // Used in WebGLBackgroundvirtualCamera.updateMatrixWorld(); virtualCamera.projectionMatrix.copy(camera.projectionMatrix);// Update the texture matrix textureMatrix.set( 0.5, 0.0, 0.0, 0.5, 0.0, 0.5, 0.0, 0.5, 0.0, 0.0, 0.5, 0.5, 0.0, 0.0, 0.0, 1.0 ); textureMatrix.multiply(virtualCamera.projectionMatrix); textureMatrix.multiply(virtualCamera.matrixWorldInverse); textureMatrix.multiply(mesh.matrixWorld);// Now update projection matrix with new clip plane, implementing code from: http://www.terathon.com/code/oblique.html // Paper explaining this technique: http://www.terathon.com/lengyel/Lengyel-Oblique.pdf reflectorPlane.setFromNormalAndCoplanarPoint( normal, reflectorWorldPosition ); reflectorPlane.applyMatrix4(virtualCamera.matrixWorldInverse);clipPlane.set( reflectorPlane.normal.x, reflectorPlane.normal.y, reflectorPlane.normal.z, reflectorPlane.constant );const projectionMatrix virtualCamera.projectionMatrix;q.x (Math.sign(clipPlane.x) projectionMatrix.elements[8]) / projectionMatrix.elements[0]; q.y (Math.sign(clipPlane.y) projectionMatrix.elements[9]) / projectionMatrix.elements[5]; q.z -1.0; q.w (1.0 projectionMatrix.elements[10]) / projectionMatrix.elements[14];// Calculate the scaled plane vector clipPlane.multiplyScalar(2.0 / clipPlane.dot(q));// Replacing the third row of the projection matrix projectionMatrix.elements[2] clipPlane.x; projectionMatrix.elements[6] clipPlane.y; projectionMatrix.elements[10] clipPlane.z 1.0 - clipBias; projectionMatrix.elements[14] clipPlane.w;// Render // TODO : 于一体的反光 不能将自己隐去 只是不显示反射纹理 mesh.visible false;const currentRenderTarget renderer.getRenderTarget();const currentXrEnabled renderer.xr.enabled; const currentShadowAutoUpdate renderer.shadowMap.autoUpdate;renderer.xr.enabled false; // Avoid camera modification renderer.shadowMap.autoUpdate false; // Avoid re-computing shadowsrenderer.setRenderTarget(renderTarget);renderer.state.buffers.depth.setMask(true); // make sure the depth buffer is writable so it can be properly cleared, see #18897if (renderer.autoClear false) renderer.clear();// filteroptions.filter.forEach((name) { const mesh scene.getObjectByName(name); mesh.visible false; });renderer.render(scene, virtualCamera);options.filter.forEach((name) { const mesh scene.getObjectByName(name); mesh.visible true; });renderer.xr.enabled currentXrEnabled; renderer.shadowMap.autoUpdate currentShadowAutoUpdate;renderer.setRenderTarget(currentRenderTarget);// Restore viewportconst viewport camera.viewport;if (viewport ! undefined) { renderer.state.viewport(viewport); }mesh.visible true; }; }new GLTFLoader().load(FILE_HOST /files/model/floor.glb, gltf {scene.add(gltf.scene)gltf.scene.traverse(child {if (child.isMesh) {addReflectorEffect(child, {color: 0xffffff,clipBias: 0.003,filter: []})}})})完整源码GitHub小结本文提供模型反射效果完整 Three.js 源码与在线 Demo建议先运行案例再改 uniform/参数做二次实验更多 Three.js 实战案例见 three-cesium-examples 合集 与 GitHub 开源仓库