ThreeJS Shader的效果样例飞线、粒子和模型轮廓高亮(三)
一、飞线效果
功能说明:支持设置点的个数,飞线速度、起始和终止颜色值、线宽、线的大小
原理:
1. 首先绘制一条与线长度相同的线,线中各点的大小逐渐变小
2. 如何让线动起来?假设点的个数总共为num个,传入的点的下标为a,通过变化的时间计算出移动的下标b,如果a+b>=num则代表,该点可见,否则不可见
3. 如何设置移动速度?传入的uTime变化在毫秒之间,通过乘以一个具体的数据,则可以将变化的时间放大,改变乘以的数据是移动速度的关键
4. 改变飞线大小?初始设置飞线的大小是整条线,通过控制飞线的结束位置以及调整点变化的速度就可以调整飞线的大小
/** * 创建飞线 * 支持设置起始和终止颜色值、飞线长度、线宽等属性、移动速率 */ function addFlyLine() { const vertex = ` attribute float aIndex; uniform float uTime; uniform float uNum; // 线上点个数 uniform float uWidth; // 线宽 uniform float uLength; // 线宽 uniform float uSpeed; // 飞线速度 varying float vIndex; // 内部点下标 void main() { vec4 viewPosition = viewMatrix * modelMatrix * vec4(position, 1.0); gl_Position = projectionMatrix * viewPosition; vIndex = aIndex; // 通过时间点的增加作为点移动的下标,从末端的第一个点开始,已num点为一个轮回,往复执行运动 float num = mod(floor(uTime * uSpeed * 100.0), uNum); // 只绘制部分点,多余的不绘制 if (aIndex + num >= uNum) { float size = (uNum - mod(aIndex + num, uNum) * (1.0 / uLength)) / uNum * uWidth; gl_PointSize = size; } } `; const frag = ` varying float vIndex; uniform float uTime; uniform float uLength; // 线宽 uniform float uNum; uniform float uSpeed; uniform vec3 uSColor; uniform vec3 uEColor; void main() { // 默认绘制的点是方形的,通过舍弃可以绘制成圆形 float distance = length(gl_PointCoord - vec2(0.5, 0.5)); if (distance > 0.5) { discard; } float num = mod(floor(uTime * uSpeed * 100.0), uNum); // 根据点的下标计算渐变色值 vec3 color = mix(uSColor, uEColor, (num + vIndex - uNum) / uNum); // 越靠近末端透明度越大 float opacity = (uNum - (num + vIndex - uNum)) / uNum; // 根据长度计算显示点的个数,多余的透明显示 if (vIndex + num >= uNum && vIndex + num <= uNum * (1.0 + uLength)) { gl_FragColor = vec4(color, opacity); } else { gl_FragColor = vec4(color, 0); } } `; const nums = 500; const curve = new THREE.CubicBezierCurve3( new THREE.Vector3(-6.0, 0.0, 0.0), new THREE.Vector3(0.0, 3.0, 9.0), new THREE.Vector3(4.0, 5.0, 0.0), new THREE.Vector3(6.0, 4.0, 3.0) ); const curveArr = curve.getPoints(nums); const flatArr = curveArr.map(e => e.toArray()); const lastArr = flatArr.flat(); const indexArr = [...Array(nums + 1).keys()]; const geometry = new THREE.BufferGeometry(); geometry.setAttribute('position', new THREE.BufferAttribute(new Float32Array(lastArr), 3)); geometry.setAttribute('aIndex', new THREE.BufferAttribute(new Float32Array(indexArr), 1)); const material = new THREE.LineBasicMaterial({ color: 0x0000ff }); // 创建一根曲线 const curveObject = new THREE.Line(geometry, material); // scene.add(curveObject); const uniform = { uTime: { value: 0.01 }, uSColor: { value: new THREE.Color(0x4effd6) }, uEColor: { value: new THREE.Color(0xffffff) }, uWidth: { value: 6.0 }, uNum: { value: nums }, uSpeed: { value: 2 }, uLength: { value: 0.15 } } setShader(geometry, vertex, frag, undefined, uniform, true) }
function setShader(geometry, vertexShader, fragmentShader, position = [0, 0, 0], uniforms = {}, isLine = false) { material = new THREE.ShaderMaterial({ vertexShader: vertexShader, fragmentShader: fragmentShader, side: THREE.DoubleSide, uniforms: uniforms, transparent: true }); let planeMesh = new THREE.Mesh(geometry, material); if (isLine) { planeMesh = new THREE.Points(geometry, material); } planeMesh.position.x = position[0]; planeMesh.position.y = position[1]; planeMesh.position.z = position[2]; scene.add(planeMesh); }
二、实现粒子效果
实现原理:
1. 根据AI生成一个特殊形状的点模型
2. 通过 distance 将立方体点改为球形
3. 计算点的距离,越远的点透明度越高,增加层次感
/** * 创建一个立体的涵道,根据传入的时间自动缩放 */ function addHole() { const vertex = ` varying vec3 vColor; uniform float uTime; void main() { float scale = mod(uTime / 3.0 + 0.3, 1.0); vec4 viewPosition = viewMatrix * modelMatrix * vec4(position, 1.0); gl_Position = projectionMatrix * viewPosition * vec4(vec3(scale), 1.0); gl_PointSize = 6.0; } `; const frag = ` void main() { float distance = length(gl_PointCoord.xy - 0.5); // 默认绘制的点是立方体的,通过判断uv点的距离大小可以绘制出球形 if (distance > 0.5) { discard; } // 边缘模糊化,点的半径较大时效果比较明显 float opacity = smoothstep(0.5, 0.1, distance); gl_FragColor = vec4(0.24, 0.12, 0.96, opacity); } `; // 创建一个自定义的几何体 const geometry = new THREE.BufferGeometry(); const vertices = []; const indices = []; // 使用三维参数方程生成心形的顶点 const segmentsU = 100; const segmentsV = 30; for (let i = 0; i <= segmentsU; i++) { const u = (i / segmentsU) * 2 * Math.PI; for (let j = 0; j <= segmentsV; j++) { const v = (j / segmentsV) * 2 * Math.PI; const x = (1 + Math.cos(v)) * Math.cos(u); const y = (1 + Math.cos(v)) * Math.sin(u); const z = Math.sin(v); vertices.push(x, y, z); } } // 生成索引数据 for (let i = 0; i < segmentsU; i++) { for (let j = 0; j < segmentsV; j++) { const a = i * (segmentsV + 1) + j; const b = (i + 1) * (segmentsV + 1) + j; const c = (i + 1) * (segmentsV + 1) + j + 1; const d = i * (segmentsV + 1) + j + 1; indices.push(a, b, d); indices.push(b, c, d); } } // 将顶点数据和索引数据添加到几何体中 geometry.setAttribute('position', new THREE.Float32BufferAttribute(vertices, 3)); geometry.setIndex(indices); setShader(geometry, vertex, frag, undefined, { uTime: { value: 0.01 } }, true) }
function setShader(geometry, vertexShader, fragmentShader, position = [0, 0, 0], uniforms = {}, isLine = false) { material = new THREE.ShaderMaterial({ vertexShader: vertexShader, fragmentShader: fragmentShader, side: THREE.DoubleSide, uniforms: uniforms, transparent: true, blending: THREE.AdditiveBlending, // 多个元素的颜色相互叠加,颜色可能会变亮,会叠加setClearColor设置的背景色 }); material.depthTest = true; material.depthWrite = false; let planeMesh = new THREE.Mesh(geometry, material); if (isLine) { planeMesh = new THREE.Points(geometry, material); } planeMesh.position.x = position[0]; planeMesh.position.y = position[1]; planeMesh.position.z = position[2]; scene.add(planeMesh); }
三、加载的模型如何添加轮廓高亮和辉光效果
EffectComposer
是ThreeJS中实现后期处理的核心工具,通过将多个处理通道(Pass)组合在一起,实现复杂的效果,比如模糊、辉光、颜色校正,需要注意,Pass顺序不同,渲染效果也会不同
常见的Pass:
1. OutlinePass:实现对象轮廓高亮效果的后期处理通道。使用它在场景中突出显示特定的对象,使其轮廓更加明显
2. RenderPass:基本通道,将场景渲染添加到渲染目标中,是后期处理的第一个通道
3. UnrealBloomPass:用于创建辉光效果,通过提取高亮部分对模型等进行模糊处理实现的辉光效果
4. GlitchPass:用于创建故障效果,模拟信号干扰的效果
5. FilmPass:模拟胶片效果,包括噪点和扫描线
6. DotScreenPass:创建点阵效果,模拟老式电视的点阵显示
7. ShaderPass:使用自定义的着色器进行效果处理
8. SMAAPass:实现子像素形态抗锯齿(SMAA)效果,提高图像的抗锯齿质量
注:如果设置的颜色和实际显示的颜色有区别,很有可能是配置的底色叠加造成的,可以把底色设置成纯黑色,其实Pass底层实现的高亮效果也是通过shader实现的
/** * 模型添加高亮效果 */ function addEffect() { const loader = new GLTFLoader(); loader.load('./Objects/parking/model/car5.gltf', (gltf) => { gltf.scene.scale.set(0.5, 0.5, 0.5); scene.add(gltf.scene); composer = new EffectComposer(renderer); const renderPass = new RenderPass(scene, camera); composer.addPass(renderPass); // 特效渲染 const outlinePass = new OutlinePass(new THREE.Vector2(window.innerWidth, window.innerHeight), scene, camera); composer.addPass(outlinePass); // 加入高管特效 outlinePass.selectedObjects = gltf.scene.children[0].children; outlinePass.pulsePeriod = 2; // 数值越大,规律越慢 outlinePass.visibleEdgeColor.set(0x00ffff); // 高光颜色 outlinePass.hiddenEdgeColor.set(0x00ff00); // 阴影颜色 outlinePass.usePatternTexture = false; // 是否使用纹理覆盖 outlinePass.edgeStrength = 2; // 高光边缘强度 outlinePass.edgeGlow = 6; // 边缘微光强度 outlinePass.edgeThickness = 2; // 高光厚度 }); }
/** * 模型添加高亮效果 */ function addEffect() { const loader = new GLTFLoader(); loader.load('./Objects/parking/model/car5.gltf', (gltf) => { gltf.scene.scale.set(0.5, 0.5, 0.5); scene.add(gltf.scene); composer = new EffectComposer(renderer); const renderPass = new RenderPass(scene, camera); composer.addPass(renderPass); // 特效渲染 const bloomPass = new UnrealBloomPass(new THREE.Vector2(window.innerWidth, window.innerHeight), 1.5, 0.4, 0.85); bloomPass.threshold = 0.1; bloomPass.strength = 1.5; bloomPass.radius = 0.5; composer.addPass(bloomPass); }
/** * 模型添加高亮效果 */ function addEffect() { const geometry = new THREE.SphereGeometry(5, 64, 20); const material = new THREE.MeshBasicMaterial({ color: 0x00ffff, side: THREE.DoubleSide }); // 创建一根曲线 const curveObject = new THREE.Mesh(geometry, material); scene.add(curveObject); composer = new EffectComposer(renderer); const renderPass = new RenderPass(scene, camera); composer.addPass(renderPass); // 特效渲染 const outlinePass = new UnrealBloomPass(new THREE.Vector2(window.innerWidth, window.innerHeight), 1.5, 0.4, 0.85); composer.addPass(outlinePass); // 加入高管特效 outlinePass.selectedObjects = [curveObject]; outlinePass.threshold = 0.1; outlinePass.strength = 1.5; outlinePass.radius = 0.5; }