基于Three.js(Threlte)实现反向半色调效果的技术求助
实现反向半色调遮罩效果(Three.js/Threlte)
问题背景
已基于Three.js+Threlte实现带偏移彩色层、叠加混合模式的多层点阵效果,但当前点阵自身携带颜色,需调整为点阵作为遮罩,让后方彩色平面透过点阵区域显示,同时保留simplexNoise生成的点阵位置逻辑,颜色可微调,优先避免使用纹理(防止画质损失)。
解决方案思路
核心通过深度遮罩机制实现:
- 先渲染点阵作为遮罩模板(仅写入深度缓冲,不输出颜色)
- 再渲染彩色大平面,仅在与遮罩深度一致的区域显示内容
- 保留原有层偏移和叠加混合模式逻辑
Threlte/Svelte 修改后代码
<script lang="ts"> import { T } from '@threlte/core'; import type { InstancedMesh, PerspectiveCamera, Mesh } from 'three'; import { onMount } from 'svelte'; import * as THREE from 'three'; import { createNoise2D } from 'simplex-noise'; // 基础参数配置 const gridSize = 120; const dotSpacing = 0.25; const maxDotSize = 0.12; const minDotSize = 0; const stackCount = 3; const yOffset = 0.24; const stackColors = ['rgb(0,0,255)', 'rgb(0,255,0)', 'rgb(255,0,0)']; const instanceCount = gridSize * gridSize * stackCount; // 生成点阵位置与大小 const simplex = createNoise2D(); const positions: Array<[number, number, number]> = []; const scales: number[] = []; for (let x = 0; x < gridSize; x++) { for (let y = 0; y < gridSize; y++) { const xNorm = x / gridSize; const yNorm = y / gridSize; const noise = simplex(xNorm * 4, yNorm * 4); const normalized = (noise + 1) / 2; const exponent = 3; const dotSize = minDotSize + (maxDotSize - minDotSize) * Math.pow(normalized, exponent); const xPos = (x - gridSize / 2) * dotSpacing; const yPos = (y - gridSize / 2) * dotSpacing; // 给每层加微小Z偏移,避免Z-fight for (let i = 0; i < stackCount; i++) { positions.push([xPos, yPos + i * yOffset, 0.1 * i]); scales.push(dotSize); } } } let camera: PerspectiveCamera; let maskMesh: InstancedMesh; onMount(() => { if (!maskMesh) return; const dummy = new THREE.Matrix4(); for (let i = 0; i < instanceCount; i++) { const [x, y, z] = positions[i]; const scale = scales[i]; dummy.makeTranslation(x, y, z); dummy.scale(new THREE.Vector3(scale, scale, 1)); maskMesh.setMatrixAt(i, dummy); } maskMesh.instanceMatrix.needsUpdate = true; }); </script> <T.PerspectiveCamera bind:ref={camera} makeDefault position={[0, 0, 10]} fov={50} /> <!-- 遮罩层:仅写入深度缓冲,作为显示模板 --> <T.InstancedMesh args={[null, null, instanceCount]} bind:ref={maskMesh} frustumCulled={false} renderOrder={0} > <T.CircleGeometry args={[1, 25]} /> <T.MeshBasicMaterial color="white" toneMapped={false} colorWrite={false} <!-- 不输出颜色,只写深度 --> writeDepth={true} /> </T.InstancedMesh> <!-- 彩色平面层:仅在遮罩区域显示,叠加混合 --> {#each stackColors as color, index} <T.Mesh renderOrder={index + 1}> <T.PlaneGeometry args={[gridSize * dotSpacing, gridSize * dotSpacing]} /> <T.MeshBasicMaterial color={color} blending={THREE.AdditiveBlending} toneMapped={false} depthFunc={THREE.EqualDepth} <!-- 仅与遮罩深度一致的区域显示 --> /> <T.Position y={index * yOffset} z={0.1 * index} /> </T.Mesh> {/each}
纯Three.js 实现方案
import * as THREE from 'three'; import { createNoise2D } from 'simplex-noise'; // 初始化场景、相机、渲染器 const scene = new THREE.Scene(); const camera = new THREE.PerspectiveCamera(50, window.innerWidth / window.innerHeight, 0.1, 1000); camera.position.z = 10; const renderer = new THREE.WebGLRenderer(); renderer.setSize(window.innerWidth, window.innerHeight); document.body.appendChild(renderer.domElement); // 参数配置 const gridSize = 120; const dotSpacing = 0.25; const maxDotSize = 0.12; const minDotSize = 0; const stackCount = 3; const yOffset = 0.24; const stackColors = ['rgb(0,0,255)', 'rgb(0,255,0)', 'rgb(255,0,0)']; const instanceCount = gridSize * gridSize * stackCount; // 生成点阵数据 const simplex = createNoise2D(); const positions = []; const scales = []; for (let x = 0; x < gridSize; x++) { for (let y = 0; y < gridSize; y++) { const xNorm = x / gridSize; const yNorm = y / gridSize; const noise = simplex(xNorm * 4, yNorm * 4); const normalized = (noise + 1) / 2; const exponent = 3; const dotSize = minDotSize + (maxDotSize - minDotSize) * Math.pow(normalized, exponent); const xPos = (x - gridSize / 2) * dotSpacing; const yPos = (y - gridSize / 2) * dotSpacing; for (let i = 0; i < stackCount; i++) { positions.push([xPos, yPos + i * yOffset, 0.1 * i]); scales.push(dotSize); } } } // 创建遮罩实例网格 const circleGeo = new THREE.CircleGeometry(1, 25); const maskMat = new THREE.MeshBasicMaterial({ color: 0xffffff, toneMapped: false, colorWrite: false, writeDepth: true }); const maskMesh = new THREE.InstancedMesh(circleGeo, maskMat, instanceCount); maskMesh.frustumCulled = false; scene.add(maskMesh); // 设置实例矩阵 const dummy = new THREE.Matrix4(); for (let i = 0; i < instanceCount; i++) { const [x, y, z] = positions[i]; const scale = scales[i]; dummy.makeTranslation(x, y, z); dummy.scale(new THREE.Vector3(scale, scale, 1)); maskMesh.setMatrixAt(i, dummy); } maskMesh.instanceMatrix.needsUpdate = true; // 创建彩色平面层 const planeGeo = new THREE.PlaneGeometry(gridSize * dotSpacing, gridSize * dotSpacing); stackColors.forEach((color, index) => { const planeMat = new THREE.MeshBasicMaterial({ color: new THREE.Color(color), blending: THREE.AdditiveBlending, toneMapped: false, depthFunc: THREE.EqualDepth }); const plane = new THREE.Mesh(planeGeo, planeMat); plane.position.set(0, index * yOffset, 0.1 * index); scene.add(plane); }); // 渲染循环 function animate() { requestAnimationFrame(animate); renderer.render(scene, camera); } animate(); // 窗口适配 window.addEventListener('resize', () => { camera.aspect = window.innerWidth / window.innerHeight; camera.updateProjectionMatrix(); renderer.setSize(window.innerWidth, window.innerHeight); });
关键修改说明
遮罩层改造:
- 将原彩色点阵改为白色遮罩,关闭
colorWrite仅写入深度缓冲,作为显示模板 - 给每层点阵添加微小Z偏移,避免彩色平面与遮罩的Z-fighting问题
- 将原彩色点阵改为白色遮罩,关闭
彩色平面层优化:
- 用大平面替代每个实例的颜色,每个平面对应一层目标颜色
- 设置
depthFunc: THREE.EqualDepth,让平面仅在与遮罩深度完全一致的区域显示,实现遮罩效果 - 保留
AdditiveBlending实现颜色叠加效果
性能优化:
- 移除实例颜色数据,减少内存占用与计算开销
- 完全保留原有simplexNoise生成的点阵大小和位置逻辑
内容的提问来源于stack exchange,提问作者Jesse Winton
相关产品推荐
相关产品推荐

