2025 ·2025 Year· ▶ 在线运行案例
- 案例合集:三维可视化功能案例(threehub.cn)
- 开源仓库github地址:https://github.com/z2586300277/three-cesium-examples
- 400个案例代码:网盘链接
你将学到什么
- onBeforeCompile 注入 GLSL 改造内置材质
- EffectComposer 多 Pass 后期处理管线
- OrbitControls 相机轨道交互
- Canvas 动态纹理贴图
- EdgesGeometry 模型边线提取
- 自定义 ShaderPass 调色/特效
- 监听窗口
resize同步更新 camera 与 renderer
效果说明
本案例演示2025效果:用 Canvas 2D 绘制内容并实时映射为 Three.js 纹理,原场景渲染后经 EffectComposer 叠加 Bloom/模糊等全屏后期;核心用到 onBeforeCompile、EffectComposer、OrbitControls。建议先打开文首在线案例查看动态画面,再对照下方源码逐步理解。
核心概念
- Scene / Camera / WebGLRenderer构成最小渲染闭环;大场景可开
logarithmicDepthBuffer缓解 Z-fighting。 - onBeforeCompile在 Three 拼好内置 shader 后替换
#include片段,适合在 PBR 材质上叠加大屏特效。 - EffectComposer以多 Pass 链式渲染:RenderPass → 特效 Pass → 输出屏幕,替代直接
renderer.render。 - OrbitControls提供轨道旋转/缩放;开启
enableDamping后需在 animate 中controls.update()。 - CanvasTexture每帧或按需把 2D Canvas 内容上传 GPU,适合动态文字、图表、视频帧贴图。
实现步骤
- 搭建灯光与环境(如有)
- requestAnimationFrame 循环 update + render
代码要点
import * as THREE from "three";import { OrbitControls } from "three/addons/controls/OrbitControls.js";
import { EffectComposer } from 'three/addons/postprocessing/EffectComposer.js'; import { RenderPass } from 'three/addons/postprocessing/RenderPass.js'; import { ShaderPass } from 'three/addons/postprocessing/ShaderPass.js'; import { VignetteShader } from 'three/addons/shaders/VignetteShader.js'; import { GammaCorrectionShader } from 'three/addons/shaders/GammaCorrectionShader.js';
let rotations =
// https://forum.gamemaker.io/index.php?threads/solved-3d-rotations-with-a-shader-matrix-or-a-matrix-glsl-es.61064/ mat4 rx(float a) { return mat4( 1.0, 0.0, 0.0, 0.0, 0.0,cos(a),sin(a), 0.0, 0.0,-sin(a),cos(a), 0.0, 0.0, 0.0, 0.0, 1.0); }mat4 ry(float a){ return mat4( cos(a),0.0, -sin(a), 0.0, 0.0, 1.0, 0.0, 0.0, sin(a), 0.0, cos(a), 0.0, 0.0,0.0,0.0,1.0); } mat4 rz(float a){ return mat4( cos(a), sin(a), 0.0, 0.0, -sin(a), cos(a), 0.0, 0.0, 0.0 , 0.0 ,1.0 ,0.0, 0.0 , 0.0 ,0.0 ,1.0); } /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// mat4 rotationXYZ(vec3 angles){ return rz(angles.z)ry(angles.y)rx(angles.x); };// reference paul https://codepen.io/prisoner849/pen/gbYgjom class Postprocessing extends EffectComposer{ constructor(){ super(renderer, new THREE.WebGLRenderTarget(innerWidth, innerHeight, {samples: 1})); let renderPass = new RenderPass(scene, camera); let effectVignette = new ShaderPass(VignetteShader); effectVignette.uniforms[ 'offset' ].value = 0.2; effectVignette.uniforms[ 'darkness' ].value = 10; let gammaCorrection = new ShaderPass( GammaCorrectionShader ); //console.log(effectVignette.material); effectVignette.material.onBeforeCompile = shader => { shader.uniforms.time = gu.time; shader.uniforms.aspect = gu.aspect; shader.uniforms.texWriting = { value: (() => { let c = document.createElement("canvas"); c.width = 2048; c.height = 512; let ctx = c.getContext("2d"); let u = val => valc.height0.01; ctx.fillStyle =
rgba(0, 255, 0, 0.5); ctx.strokeStyle =rgba(255, 0, 0, 1); ctx.lineWidth = u(1.25); ctx.lineJoin = "round"; ctx.lineCap = "round"; ctx.font =${u(45)}px BerkshireSwash; ctx.textAlign = "center"; ctx.textBaseline = "middle"; //ctx.strokeRect(0, 0, c.width, c.height); let text =Happy New Year!; ctx.fillText(text, c.width0.5, c.height0.5); ctx.strokeText(text, c.width0.5, c.height0.5); let tex = new THREE.CanvasTexture(c); tex.colorSpace = "srgb"; return tex; })() } shader.fragmentShader =uniform float time; uniform float aspect; uniform sampler2D texWriting; float N21(vec2 p) { p = fract(p * vec2(233.34, 851.73)); p += dot(p, p + 23.45); return fract(p.x * p.y); } vec2 N22(vec2 p){ vec3 a = fract(p.xyx * vec3(123.45, 234.56, 345.67)); a += dot(a, a+45.69); return fract(vec2(a.xa.y, a.ya.z)); } ${shader.fragmentShader}.replace(gl_FragColor = vec4( mix( texel.rgb, vec3( 1.0 - darkness ), dot( uv, uv ) ), texel.a );,gl_FragColor = vec4( mix( texel.rgb, vec3( 1.0 - darkness ), dot( uv, uv ) ), texel.a ); vec2 texUV = (vUv - 0.5) * vec2(aspect, 4.) + 0.5; texUV.y += 1.7;vec4 col = texture(texWriting, texUV); // cells float t = time * 0.1; vec2 cellsUV = (texUV + vec2(0., t))vec2(4., 1.)40.; vec2 cId = floor(cellsUV); vec2 cUv = fract(cellsUV); // particles float c = 0.; for(float y = -2.; y <= 2.;y++){ for(float x = -2.; x <= 2.; x++){ vec2 offs = vec2(x, y); vec2 locId = mod(cId + offs, vec2(100.)); float n1 = N21(locId); float rr = (n10.5 + 0.5)0.5; vec2 shift = N22(locId) + offs; shift.x += sin(mod((n1 + t2.n1)${Math.PI2}, ${Math.PI2}))1.25; // swaying c = max(c, smoothstep(rr, rr - 0.1, length(shift - cUv))); } } // fill float fFill = c * col.g; float fDraw = max(fFill, col.r); gl_FragColor.rgb = mix(gl_FragColor.rgb, vec3(0.9875, 1, 1) * 0.9, fDraw);); console.log(shader.fragmentShader); } this.addPass(renderPass); this.addPass(gammaCorrection); this.addPass(effectVignette); } }class BackShards extends THREE.LineSegments{ constructor(){ let g = new THREE.InstancedBufferGeometry().copy( new THREE.EdgesGeometry(new THREE.TetrahedronGeometry()) ); g.instanceCount = 100; let v = new THREE.Vector3(); g.setAttribute("instPos", new THREE.InstancedBufferAttribute( new Float32Array( Array.from({length: g.instanceCount}, () => { v.random().subScalar(0.5).multiply(new THREE.Vector3(40, 20, 0)); return [v.x, v.y, v.z]; }).flat() ) ,3 )); g.setAttribute("instRotPhase", new THREE.InstancedBufferAttribute( new Float32Array( Array.from({length: g.instanceCount}, () => { v.random().subScalar(0.5).multiplyScalar(2); return [v.x, v.y, v.z]; }).flat() ) ,3 )); g.setAttribute("instRotSpeed", new THREE.InstancedBufferAttribute( new Float32Array( Array.from({length: g.instanceCount}, () => { v.random().multiplyScalar(0.1).addScalar(0.1).multiplyScalar(2); return [v.x, v.y, v.z]; }).flat() ) ,3 )); let m = new THREE.LineBasicMaterial({ color: 0x004499, onBeforeCompile: shader => { shader.uniforms.time = gu.time; shader.vertexShader =
uniform float time; attribute vec3 instPos; attribute vec3 instRotPhase; attribute vec3 instRotSpeed; ${rotations} ${shader.vertexShader}.replace(#include,#include mat4 rotXYZ = rotationXYZ((instRotPhase + instRotSpeedtime1.normalize(instRotPhase))${Math.PI}); transformed = vec3(rotXYZ * vec4(transformed, 1.)); transformed += instPos;); //console.log(shader.vertexShader); } }); super(g, m); } update(){ camera.getWorldDirection(this.position); this.position.setLength(22).add(camera.position); this.quaternion.copy(camera.quaternion); } }class Shards extends THREE.Mesh{ constructor(){ super(); let g = new THREE.InstancedBufferGeometry().copy(new THREE.TetrahedronGeometry()); g.instanceCount = 10000; let v = new THREE.Vector3(); g.setAttribute("center", new THREE.Float32BufferAttribute( [ 1, 0, 0, 0, 1, 0, 0, 0, 1, 1, 0, 0, 0, 1, 0, 0, 0, 1, 1, 0, 0, 0, 1, 0, 0, 0, 1, 1, 0, 0, 0, 1, 0, 0, 0, 1 ], 3 )); g.setAttribute("instPos", new THREE.InstancedBufferAttribute( new Float32Array( Array.from({length: g.instanceCount}, () => { let a = Math.random()Math.PI2; v.set( Math.cos(a), Math.sin(a), Math.random() ); return [v.x, v.y, v.z]; }).flat() ) ,3 )); g.setAttribute("instSca", new THREE.InstancedBufferAttribute( new Float32Array( Array.from({length: g.instanceCount}, () => { v.random().multiplyScalar(0.5).addScalar(0.5); return [v.x, v.y, v.z]; }).flat() ) ,3 )); g.setAttribute("instRotPhase", new THREE.InstancedBufferAttribute( new Float32Array( Array.from({length: g.instanceCount}, () => { v.random().subScalar(0.5).multiplyScalar(2); return [v.x, v.y, v.z]; }).flat() ) ,3 )); g.setAttribute("instRotSpeed", new THREE.InstancedBufferAttribute( new Float32Array( Array.from({length: g.instanceCount}, () => { v.random().multiplyScalar(0.1).addScalar(0.1).multiplyScalar(2); return [v.x, v.y, v.z]; }).flat() ) ,3 )); let m = new THREE.MeshStandardMaterial({ color: 0x4488ff, emissive: 0x2266ff, roughness: 0.25, metalness: 0.9, envMapIntensity: 0.1, onBeforeCompile: shader => { shader.uniforms.time = gu.time; shader.uniforms.texDate = {value: (() => { let c = document.createElement("canvas"); c.width = c.height = 1024; let u = val => valc.height0.01; let ctx = c.getContext("2d"); let fSize = 45; ctx.font =
${u(fSize)}px Limelight; ctx.fillStyle =rgba(0, 0, 0, 1); ctx.strokeStyle =rgba(255, 255, 255, 1); ctx.lineWidth = u(3); ctx.textAlign = "center"; ctx.textBaseline = "middle"; ctx.fillRect(0, 0, c.width, c.height); //ctx.fillStyle =rgba(255, 255, 255, 1); [ {letter: "2", pos: new THREE.Vector2(-1, -1)}, {letter: "0", pos: new THREE.Vector2( 1, -1)}, {letter: "2", pos: new THREE.Vector2(-1, 1)}, {letter: "5", pos: new THREE.Vector2( 1, 1)} ].forEach(item => { let itemX = u(item.pos.x * 17 + 50); let itemY = u(item.pos.y * 20 + 52); ctx.strokeText(item.letter, itemX, itemY); //ctx.fillText(item.letter, itemX, itemY); }) let tex = new THREE.CanvasTexture(c); return tex; })()} shader.vertexShader =uniform float time; uniform sampler2D texDate; attribute vec3 center; attribute vec3 instPos; attribute vec3 instSca; attribute vec3 instRotPhase; attribute vec3 instRotSpeed; varying vec3 vCenter; varying float vTexColor; // 2D Random float random (in vec2 st) { return fract(sin(dot(st.xy, vec2(12.9898,78.233)))43758.5453123);
} // 2D Noise based on Morgan McGuire @morgan3d // https://www.shadertoy.com/view/4dS3Wd float noise (in vec2 st) { vec2 i = floor(st); vec2 f = fract(st);// Four corners in 2D of a tile float a = random(i); float b = random(i + vec2(1.0, 0.0)); float c = random(i + vec2(0.0, 1.0)); float d = random(i + vec2(1.0, 1.0));
// Smooth Interpolation
// Cubic Hermine Curve. Same as SmoothStep() vec2 u = ff(3.0-2.0*f); // u = smoothstep(0.,1.,f);
// Mix 4 coorners percentages return mix(a, b, u.x) + (c - a)u.y(1.0 - u.x) + (d - b)u.xu.y; } float getNoise(vec2 st, float time){ float a = atan(st.y, st.x) + ${Math.PI * 0.5}; float r = length(st); float aStep = ${Math.PI} / 3.; float aPart = abs(mod(a, aStep) - (0.5 * aStep)); vec2 suv = vec2(cos(aPart), sin(aPart)) * r - vec2(time, 0.); float n = noise(suv); return n; } ${rotations} ${shader.vertexShader}.replace(#include,#include mat4 rotXYZ = rotationXYZ((instRotPhase + instRotSpeedtime1.normalize(instRotPhase))${Math.PI}); objectNormal = vec3(rotXYZ * vec4(objectNormal, 1.));).replace(#include,#include vCenter = center;transformed = vec3(rotXYZ * vec4(transformed, 1.)); float t = time * 0.25; float maxR = 5.; float currLen = mod(instPos.z * maxR + t, maxR); vec3 iPos = vec3(instPos.xy * currLen, 0.); float nz = clamp(getNoise(iPos.xy2., t4.), 0., 1.); nz= nznznznz; float sideSize = maxR * 0.9; vec2 texUV = (vec2(iPos.xy) + sideSize ) / (sideSize * 2.); vec4 texVals = texture(texDate, texUV); float nf = nz(1. - smoothstep(maxR0.8, maxR, currLen)); vTexColor = texVals.g; nf = max(nf2., texVals.g0.25); transformed *= instSca; transformed= 0.5nf; transformed += iPos; transformed.z += vTexColor * 0.5;); //console.log(shader.vertexShader); shader.fragmentShader =varying vec3 vCenter; varying float vTexColor; ${shader.fragmentShader}.replace(vec3 totalEmissiveRadiance = emissive;,vec3 totalEmissiveRadiance = mix(emissive, vec3(1), vTexColor); float edgeThickness = 1. + vTexColor * 0.25; vec3 afwidth = fwidth( vCenter.xyz );vec3 edge3 = smoothstep( ( edgeThickness - 1.0 )afwidth, edgeThicknessafwidth, vCenter.xyz );
float edge = 1.0 - min( min( edge3.x, edge3.y ), edge3.z ); totalEmissiveRadiance *= edge;).replace(#include,roughnessFactor += vTexColor * 0.25; metalnessFactor -= vTexColor * 0.5; #include); //console.log(shader.fragmentShader); } }); this.geometry = g; this.material = m; } }let gu = { time: {value: 0}, aspect: {value: innerWidth / innerHeight} } let dpr = Math.min(devicePixelRatio, 1); let scene = new THREE.Scene(); scene.background = new THREE.Color(0x002f55); let camera = new THREE.PerspectiveCamera(45, innerWidth / innerHeight, 0.1, 100); camera.position.set(0, 0, 1).setLength(12); let renderer = new THREE.WebGLRenderer({ antialias: true }); renderer.setPixelRatio(devicePixelRatio); renderer.setSize(innerWidthdpr, innerHeightdpr); renderer.toneMapping = THREE.ACESFilmicToneMapping; document.body.appendChild(renderer.domElement);
let postprocessing = new Postprocessing();
window.addEventListener("resize", (event) => { camera.aspect = innerWidth / innerHeight; camera.updateProjectionMatrix(); gu.aspect.value = camera.aspect; renderer.setSize(innerWidthdpr, innerHeightdpr); postprocessing.setSize(innerWidthdpr, innerHeightdpr); });
let controls = new OrbitControls(camera, renderer.domElement); controls.enableDamping = true; controls.enablePan = false; controls.minAzimuthAngle = -Math.PI * 0.25; controls.maxAzimuthAngle = Math.PI * 0.24; controls.minPolarAngle = Math.PI * 0.25; controls.maxPolarAngle = Math.PI * 0.75; controls.minDistance = controls.getDistance() * 0.25; controls.maxDistance = controls.getDistance();
let lightAmount = 3; for(let i = 0; i < lightAmount; i++){ let light = new THREE.DirectionalLight(0xffffff, Math.PI / lightAmount * 2); light.position.setScalar(1).applyAxisAngle(new THREE.Vector3(0, 0, 1), Math.PI2 / lightAmounti); scene.add(light); } scene.add(new THREE.AmbientLight(0xffffff, Math.PI * 0.5));
let shards = new Shards(); scene.add(shards);
let backShards = new BackShards(); scene.add(backShards);
let clock = new THREE.Clock(); let t = 0;
renderer.setAnimationLoop(() => { let dt = clock.getDelta(); t += dt; gu.time.value = t; controls.update(); backShards.update(); //renderer.render(scene, camera); postprocessing.render(); });
完整源码:GitHub
小结
- 本文提供2025完整 Three.js 源码与在线 Demo,建议先运行案例再改 uniform/参数做二次实验
- 更多 Three.js 实战案例见 three-cesium-examples 合集 与 GitHub 开源仓库