cocos3.8,动态擦除3d效果,橡皮擦功能

当前效果,就是把脏的贴图擦除,显示出干净的贴图,根据某个世界坐标,或者玩家的世界坐标,动态修改当前坐标半径内的擦除遮罩,来实现擦除功能,代码中包含擦除进度,还有一键全部擦除的功能

1.shader代码

复制代码
// Effect Syntax Guide: https://docs.cocos.com/creator/manual/zh/shader/index.html

CCEffect %{
  techniques:
  - name: opaque
    passes:
    - vert: standard-vs
      frag: standard-fs
      properties: &props
        mainTexture:           { value: grey, target: albedoMap, editor: { displayName: Dirt Texture (污垢) } }
        cleanTexture:          { value: white, target: cleanMap, editor: { displayName: Clean Texture (干净) } }
        maskTexture:           { value: black, target: maskMap, editor: { displayName: Mask Texture (擦除遮罩) } }
        mainColor:                { value: [1.0, 1.0, 1.0, 1.0], target: albedo, linear: true, editor: { displayName: Albedo, type: color } }
        albedoScale:              { value: [1.0, 1.0, 1.0], target: albedoScaleAndCutoff.xyz }
        alphaThreshold:           { value: 0.5, target: albedoScaleAndCutoff.w, editor: { parent: USE_ALPHA_TEST, slide: true, range: [0, 1.0], step: 0.001 } }
        roughness:                { value: 0.8, target: pbrParams.y, editor: { slide: true, range: [0, 1.0], step: 0.001 } }
        metallic:                 { value: 0.6, target: pbrParams.z, editor: { slide: true, range: [0, 1.0], step: 0.001 } }
    - &forward-add
      vert: standard-vs
      frag: standard-fs
      phase: forward-add
      propertyIndex: 0
      embeddedMacros: { CC_FORWARD_ADD: true }
      depthStencilState:
        depthFunc: equal
        depthTest: true
        depthWrite: false
      blendState:
        targets:
        - blend: true
          blendSrc: one
          blendDst: one
          blendSrcAlpha: zero
          blendDstAlpha: one
    - &shadow-caster
      vert: shadow-caster-vs
      frag: shadow-caster-fs
      phase: shadow-caster
      propertyIndex: 0
      rasterizerState:
        cullMode: front
      properties:
        mainColor:      { value: [1.0, 1.0, 1.0, 1.0], target: albedo, editor: { displayName: Albedo, type: color } }
        albedoScale:    { value: [1.0, 1.0, 1.0], target: albedoScaleAndCutoff.xyz }
        alphaThreshold: { value: 0.5, target: albedoScaleAndCutoff.w, editor: { parent: USE_ALPHA_TEST } }
        mainTexture:    { value: grey, target: albedoMap, editor: { displayName: Dirt Texture } }
  - name: transparent
    passes:
    - vert: standard-vs
      frag: standard-fs
      embeddedMacros: { CC_FORCE_FORWARD_SHADING: true }
      depthStencilState:
        depthTest: true
        depthWrite: false
      blendState:
        targets:
        - blend: true
          blendSrc: src_alpha
          blendDst: one_minus_src_alpha
          blendDstAlpha: one_minus_src_alpha
      properties: *props
    - *forward-add
    - *shadow-caster
}%


CCProgram shared-ubos %{
  uniform Constants {
    vec4 albedo;
    vec4 albedoScaleAndCutoff;
    vec4 pbrParams;
  };
}%

CCProgram macro-remapping %{
  #pragma define-meta USE_TWOSIDE
  #pragma define-meta USE_VERTEX_COLOR

  #define CC_SURFACES_USE_TWO_SIDED USE_TWOSIDE
  #define CC_SURFACES_USE_VERTEX_COLOR USE_VERTEX_COLOR
}%

CCProgram surface-vertex %{
  #define CC_SURFACES_VERTEX_MODIFY_WORLD_POS
  vec3 SurfacesVertexModifyWorldPos(in SurfacesStandardVertexIntermediate In)
  {
    return In.worldPos;
  }
  
  #define CC_SURFACES_VERTEX_MODIFY_WORLD_NORMAL
  vec3 SurfacesVertexModifyWorldNormal(in SurfacesStandardVertexIntermediate In)
  {
    return In.worldNormal.xyz;
  }
  
  #define CC_SURFACES_VERTEX_MODIFY_UV
  void SurfacesVertexModifyUV(inout SurfacesStandardVertexIntermediate In)
  {
  }
}%


CCProgram surface-fragment %{
  // 基础污垢贴图
  #if USE_ALBEDO_MAP
    uniform sampler2D albedoMap;
    #pragma define-meta ALBEDO_UV options([v_uv, v_uv1])
  #endif

  // 新增:干净的贴图与擦除遮罩贴图
  uniform sampler2D cleanMap;
  uniform sampler2D maskMap;

  #if USE_ALPHA_TEST
    #pragma define-meta ALPHA_TEST_CHANNEL options([a, r])
  #endif

  #define CC_SURFACES_FRAGMENT_MODIFY_BASECOLOR_AND_TRANSPARENCY
  vec4 SurfacesFragmentModifyBaseColorAndTransparency()
  {
    vec4 baseColor = albedo;
    
    // 默认采样污垢贴图
    vec4 dirtColor = vec4(1.0);
    #if USE_ALBEDO_MAP
      dirtColor = texture(albedoMap, ALBEDO_UV);
      dirtColor.rgb = SRGBToLinear(dirtColor.rgb);
    #endif

    // 采样干净贴图与遮罩贴图
    vec4 cleanColor = texture(cleanMap, ALBEDO_UV);
    cleanColor.rgb = SRGBToLinear(cleanColor.rgb);

    // 采样 Mask 贴图(假设脚本涂抹的是 R 通道)
    vec4 maskColor = texture(maskMap, ALBEDO_UV);
    float mask = maskColor.r; // 0 表示污垢,1 表示干净(或者反过来,取决于你脚本怎么画)

    // 根据遮罩进行线性插值混合
    vec4 mixedTexColor = mix(dirtColor, cleanColor, mask);
    baseColor *= mixedTexColor;

    #if USE_ALPHA_TEST
      if (baseColor.ALPHA_TEST_CHANNEL < albedoScaleAndCutoff.w) discard;
    #endif

    baseColor.rgb *= albedoScaleAndCutoff.xyz;
    return baseColor;
  }

  #define CC_SURFACES_FRAGMENT_ALPHA_CLIP_ONLY
  void SurfacesFragmentAlphaClipOnly()
  {
    #if USE_ALPHA_TEST
      float alpha = albedo.ALPHA_TEST_CHANNEL;
      #if USE_VERTEX_COLOR
        alpha *= FSInput_vertexColor.a;
      #endif
      #if USE_ALBEDO_MAP
        alpha = texture(albedoMap, ALBEDO_UV).ALPHA_TEST_CHANNEL;
      #endif
  
      if (alpha < albedoScaleAndCutoff.w) discard;
    #endif
  }

  #define CC_SURFACES_FRAGMENT_MODIFY_WORLD_NORMAL
  vec3 SurfacesFragmentModifyWorldNormal()
  {
    return normalize(FSInput_worldNormal);
  }

  #define CC_SURFACES_FRAGMENT_MODIFY_EMISSIVE
  vec3 SurfacesFragmentModifyEmissive()
  {
    return vec3(0.0, 0.0, 0.0);
  }

  #define CC_SURFACES_FRAGMENT_MODIFY_PBRPARAMS
  vec4 SurfacesFragmentModifyPBRParams()
  {
    return vec4(1.0, pbrParams.y, pbrParams.z, 0.5);
  }
}%

// 后面剩余的标准 vs/fs 桥接程序保持不变...
CCProgram standard-vs %{
  precision highp float;
  #include <macro-remapping>
  #include <surfaces/effect-macros/common-macros>
  #include <surfaces/includes/common-vs>
  #include <shared-ubos>
  #include <surface-vertex>
  #include <surfaces/includes/standard-vs>
  #include <shading-entries/main-functions/render-to-scene/vs>
}%

CCProgram shadow-caster-vs %{
  precision highp float;
  #include <surfaces/effect-macros/render-to-shadowmap>
  #include <surfaces/includes/common-vs>
  #include <shared-ubos>
  #include <surface-vertex>
  #include <shading-entries/main-functions/render-to-shadowmap/vs>
}%

CCProgram standard-fs %{
  precision highp float;
  #include <macro-remapping>
  #include <surfaces/effect-macros/common-macros>
  #include <surfaces/includes/common-fs>
  #include <shared-ubos>
  #include <surface-fragment>
  #include <lighting-models/includes/standard>
  #include <surfaces/includes/standard-fs>
  #include <shading-entries/main-functions/render-to-scene/fs>
}%

CCProgram shadow-caster-fs %{
  precision highp float;
  #include <surfaces/effect-macros/render-to-shadowmap>
  #include <surfaces/includes/common-fs>
  #include <shared-ubos>
  #include <surface-fragment>
  #include <shading-entries/main-functions/render-to-shadowmap/fs>
}%

2.把对应的贴图赋值,擦除遮罩,是一个256*256的纯黑图片

3.把材质赋值给需要擦除的物体,然后增加控制脚本,设置好擦除半径,动态根据要擦除的世界坐标(调用eraseAtWorldPosition),发出射线,更新材质的遮罩数据

控制脚本

复制代码
import { _decorator, Component, geometry, MeshRenderer, Texture2D, Vec2, Vec3, Mat4, gfx } from 'cc';
const { ccclass, property } = _decorator;

@ccclass('MeshEraser')
export class MeshEraser extends Component {
    @property(MeshRenderer)
    public meshRenderer: MeshRenderer = null!; // 目标模型的 MeshRenderer

    @property
    public brushRadius: number = 10; // 画笔半径(像素)

    @property
    public maskSize: number = 256; // 动态遮罩的分辨率

    private _maskTexture!: Texture2D;
    private _maskData!: Uint8Array;
    private _isInitialized: boolean = false;

    start() {
        this.initMask();
    }

    /** 初始化遮罩纹理 */
    private initMask() {
        // 初始化全黑遮罩数据
        const dataSize = this.maskSize * this.maskSize * 4;
        this._maskData = new Uint8Array(dataSize);
        for (let i = 0; i < dataSize; i += 4) {
            this._maskData[i] = 0;       // R: 0 (代表有污垢)
            this._maskData[i + 1] = 0;   // G
            this._maskData[i + 2] = 0;   // B
            this._maskData[i + 3] = 255; // A
        }

        this._maskTexture = new Texture2D();
        this._maskTexture.reset({
            width: this.maskSize,
            height: this.maskSize,
            format: Texture2D.PixelFormat.RGBA8888,
        });
        this._maskTexture.uploadData(this._maskData);

        const mat = this.meshRenderer.getMaterial(0);
        if (mat) {
            mat.setProperty('maskMap', this._maskTexture);
        }

        this._isInitialized = true;
    }

    /**
     * 根据世界坐标位置进行擦除(供外部调用,如 PlayerController)
     * 从给定世界位置向下发射射线,命中目标 mesh 后在对应 UV 处擦除
     * @param worldPos 世界坐标位置(如清洗机效果节点的位置)
     */
    public eraseAtWorldPosition(worldPos: Vec3) {
        if (!this._isInitialized || !this.meshRenderer || !this.meshRenderer.mesh) return;

        // 从世界位置向下发射射线
        const ray = new geometry.Ray();
        geometry.Ray.set(ray, worldPos.x, worldPos.y + 2, worldPos.z, 0, -1, 0);

        const uv = this.calculateHitUV(ray);
        if (uv) {
            this.drawOnMask(uv);
        }
    }

    // 核心:将世界空间射线转到局部空间,读取网格数据手动做射线-三角形相交计算
    private calculateHitUV(worldRay: geometry.Ray): Vec2 | null {
        const node = this.meshRenderer.node;
        const mesh = this.meshRenderer.mesh!;

        // 1. 计算逆矩阵,将世界坐标系下的射线转换到物体的局部坐标系(Local Space)
        const invWorldMatrix = new Mat4();
        Mat4.invert(invWorldMatrix, node.worldMatrix);

        const localOrigin = new Vec3();
        const localDir = new Vec3();
        Vec3.transformMat4(localOrigin, worldRay.o, invWorldMatrix);

        // 方向向量转换需要注意去掉平移影响
        const worldTarget = new Vec3();
        Vec3.add(worldTarget, worldRay.o, worldRay.d);
        Vec3.transformMat4(worldTarget, worldTarget, invWorldMatrix);
        Vec3.subtract(localDir, worldTarget, localOrigin);
        Vec3.normalize(localDir, localDir);

        // 2. 读取 Mesh 顶点、UV 和索引数据
        const positions = mesh.readAttribute(0, gfx.AttributeName.ATTR_POSITION);
        const uvs = mesh.readAttribute(0, gfx.AttributeName.ATTR_TEX_COORD);
        const indices = mesh.readIndices(0);
        if (!positions || !uvs || !indices) return null;

        let minT = Infinity;
        let finalUV = new Vec2();

        // 临时变量复用避免垃圾回收(GC)
        const v0 = new Vec3(), v1 = new Vec3(), v2 = new Vec3();
        const edge1 = new Vec3(), edge2 = new Vec3(), pvec = new Vec3(), tvec = new Vec3(), qvec = new Vec3();

        // 3. 遍历所有的三角形网格 (每3个索引组成一个面)
        for (let i = 0; i < indices.length; i += 3) {
            const idx0 = indices[i];
            const idx1 = indices[i + 1];
            const idx2 = indices[i + 2];

            // 提取三角形的三个顶点坐标
            v0.set(positions[idx0 * 3], positions[idx0 * 3 + 1], positions[idx0 * 3 + 2]);
            v1.set(positions[idx1 * 3], positions[idx1 * 3 + 1], positions[idx1 * 3 + 2]);
            v2.set(positions[idx2 * 3], positions[idx2 * 3 + 1], positions[idx2 * 3 + 2]);

            // Möller--Trumbore 射线-三角形相交算法
            Vec3.subtract(edge1, v1, v0);
            Vec3.subtract(edge2, v2, v0);
            Vec3.cross(pvec, localDir, edge2);
            const det = Vec3.dot(edge1, pvec);

            // det 接近 0 说明射线与三角形共面或平行
            if (det > -0.000001 && det < 0.000001) continue;
            const invDet = 1.0 / det;

            Vec3.subtract(tvec, localOrigin, v0);
            const u = Vec3.dot(tvec, pvec) * invDet;
            if (u < 0.0 || u > 1.0) continue;

            Vec3.cross(qvec, tvec, edge1);
            const v = Vec3.dot(localDir, qvec) * invDet;
            if (v < 0.0 || u + v > 1.0) continue;

            const t = Vec3.dot(edge2, qvec) * invDet;

            // 如果找到了更近的交点
            if (t > 0.000001 && t < minT) {
                minT = t;
                const w = 1.0 - u - v;

                // 提取三个顶点的原始 UV
                const uv0_x = uvs[idx0 * 2], uv0_y = uvs[idx0 * 2 + 1];
                const uv1_x = uvs[idx1 * 2], uv1_y = uvs[idx1 * 2 + 1];
                const uv2_x = uvs[idx2 * 2], uv2_y = uvs[idx2 * 2 + 1];

                // 重心插值算当前交点的精细 UV
                finalUV.x = uv0_x * w + uv1_x * u + uv2_x * v;
                finalUV.y = uv0_y * w + uv1_y * u + uv2_y * v;
            }
        }
        return minT !== Infinity ? finalUV : null;
    }

    private drawOnMask(uv: Vec2) {
        const centerX = Math.floor(uv.x * this.maskSize);
        const centerY = Math.floor(uv.y * this.maskSize);

        let isDirty = false;

        // 定义羽化内径比例(0.0 ~ 1.0)
        // 0.4 表示画笔中心 40% 的区域是完全擦除的纯白,外围 60% 的区域向外逐渐变淡(模糊)
        const innerRatio = 0.4;
        const innerRadius = this.brushRadius * innerRatio;

        for (let y = centerY - this.brushRadius; y <= centerY + this.brushRadius; y++) {
            for (let x = centerX - this.brushRadius; x <= centerX + this.brushRadius; x++) {
                if (x < 0 || x >= this.maskSize || y < 0 || y >= this.maskSize) continue;

                // 计算当前像素到圆心的真实距离
                const distance = Math.sqrt((x - centerX) * (x - centerX) + (y - centerY) * (y - centerY));

                // 只有在画笔半径内的像素才处理
                if (distance <= this.brushRadius) {
                    let alphaAlpha = 255;

                    if (distance <= innerRadius) {
                        // 1. 在内径以内,完全擦除(纯白)
                        alphaAlpha = 255;
                    } else {
                        // 2. 在内径和外径之间,进行平滑渐变插值 (Smoothstep)
                        // 距离越远,factor 越接近 0
                        const factor = 1.0 - (distance - innerRadius) / (this.brushRadius - innerRadius);

                        // 使用平滑三次插值,让边缘过渡更自然、更柔和
                        const smoothFactor = factor * factor * (3.0 - 2.0 * factor);
                        alphaAlpha = Math.floor(smoothFactor * 255);
                    }

                    const index = (y * this.maskSize + x) * 4;

                    // 核心:因为是多次涂抹,我们要取当前渐变值和原有值的"最大值",防止一笔把之前擦干净的地方又变脏
                    if (this._maskData[index] < alphaAlpha) {
                        this._maskData[index] = alphaAlpha;     // R
                        this._maskData[index + 1] = alphaAlpha; // G
                        this._maskData[index + 2] = alphaAlpha; // B
                        isDirty = true;
                    }
                }
            }
        }

        if (isDirty) {
            this._maskTexture.uploadData(this._maskData);
        }
    }

    /**
     * 获取当前擦除进度 (0~1)
     * 0 = 完全没擦, 1 = 全部擦干净
     */
    public getEraseProgress(): number {
        if (!this._isInitialized) return 0;
        let totalWhite = 0;
        const totalPixels = this.maskSize * this.maskSize;
        for (let i = 0; i < this._maskData.length; i += 4) {
            totalWhite += this._maskData[i]; // R channel
        }
        return totalWhite / (totalPixels * 255);
    }

    /**
     * 强制将所有区域标记为已擦除(全白),用于完成时一次性清理干净
     */
    public eraseAll() {
        if (!this._isInitialized) return;
        for (let i = 0; i < this._maskData.length; i += 4) {
            this._maskData[i] = 255;     // R
            this._maskData[i + 1] = 255; // G
            this._maskData[i + 2] = 255; // B
        }
        this._maskTexture.uploadData(this._maskData);
    }

    onDestroy() {
        if (this._maskTexture) {
            this._maskTexture.destroy();
        }
    }
}
相关推荐
云飞云共享云桌面2 小时前
集中算力・统一数据・高效协同:SolidWorks 云桌面方案详解
运维·服务器·人工智能·安全·3d·电脑·制造
私人珍藏库1 天前
[Android] 三维山水全景地图-3D地形全景观测地图
android·3d·app·工具·软件·多功能
趋之2 天前
Mars3D 三维可视化开发入门实战教程
3d
探物 AI2 天前
零基础入门3D点云深度学习:从PointNet开始,理解3D数据处理
人工智能·深度学习·3d
码来的小朋友2 天前
[Python] 制作小游戏创意之3D魔方
python·3d·pygame
pythonpioneer2 天前
PyTorch3D:基于 PyTorch 的高效 3D 深度学习工具库
pytorch·深度学习·其他·3d
大江东去浪淘尽千古风流人物3 天前
【PromptStereo】零样本立体匹配新范式:用结构与运动Prompt驱动迭代优化(CVPR 2026)
深度学习·3d·slam·视觉定位·dust3r·3d重建·mast3r
智海深蓝3 天前
海上平行战场:态势模拟三维可视化平台
3d·ue5
苏州邦恩精密3 天前
2026江苏GOM三维扫描仪定制厂家找哪家?企业数字化转型视角
人工智能·机器学习·3d·自动化·制造