You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

基于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);
});

关键修改说明

  1. 遮罩层改造:

    • 将原彩色点阵改为白色遮罩,关闭colorWrite仅写入深度缓冲,作为显示模板
    • 给每层点阵添加微小Z偏移,避免彩色平面与遮罩的Z-fighting问题
  2. 彩色平面层优化:

    • 用大平面替代每个实例的颜色,每个平面对应一层目标颜色
    • 设置depthFunc: THREE.EqualDepth,让平面仅在与遮罩深度完全一致的区域显示,实现遮罩效果
    • 保留AdditiveBlending实现颜色叠加效果
  3. 性能优化:

    • 移除实例颜色数据,减少内存占用与计算开销
    • 完全保留原有simplexNoise生成的点阵大小和位置逻辑

内容的提问来源于stack exchange,提问作者Jesse Winton

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.13 05:54:58