How can I let players "scrape away" one texture to reveal another?

In this game I’m making, I want my players to be able to remove rust from an object based on collisions from another object such as a wire brush. Since it’s a VR game, performance is key.

I’ve been having a hard time getting on the right track; I think I’m not asking the right questions. The closest I’ve gotten was a video about making a scratch-off lotto ticket. Someone in the comments to that video says:

Good Idea. But, this isn’t practical for any real world apps. It instantiates too many gameobjects. There are better more efficient ways of achieving this effect using shaders or render texture.

They don’t really expand on that further, but I did do a little more searching to see how I might do this using shaders. I found this video about using replacement shaders, but I’m still not sure if that’s the right track. I don’t know If something like that is the smartest way to do it.

I’m basically a complete noob when it comes to shaders and textures so this is new territory for me. I apologize if this is a duplicate question, I’ve tried searching but haven’t found anything quite like what I’m looking for. Please let me know if there’s anything I can explain further, and any help that gets me pointed in the right direction would be greatly appreciated.

Game Development Asked on November 21, 2021

1 Answers

One Answer

If you're okay with mesh colliders, take a look at this. The example describes a (primitive) version of what you want.

The next step would probably be to write a shader that takes three textures: a "rusty" version, a "clean" texture and a "splatmap" that stores where to apply which texture. Instead of the main texture, the script should paint into this splatmap.

All the shader needs to do is "fade" between the two textures. I'm at work right now and can't give you all the boilerplate from memory, but the relevant line should look something like this:

out.rgba = _rusty.rgba * _splat.r + _clean.rgba * (1.0 - _splat.r)

I'm using the splatmap's red channel here, any other will do as well. The splatmap can have a different resolution than your other textures so play around with that and pick whatever looks best on your object.

EDIT: Here's the final code that did the trick (made by OP). DotClick.cs should be applied to the camera and ClearableShader should be applied to the object in question.


Shader "Custom/ClearableShader"
        _Color ("Color", Color) = (1,1,1,1)
        _CleanTex("Clean Texture", 2D) = "white" {}
        _RustTex("Rust Texture", 2D) = "white"
        _SplatTex ("Splat Map", 2D) = "white"
        Tags { "RenderType"="Opaque" }
        LOD 200

        // Physically based Standard lighting model, and enable shadows on all light types
        #pragma surface surf Standard fullforwardshadows

        // Use shader model 3.0 target, to get nicer looking lighting
        #pragma target 3.0

        sampler2D _CleanTex;
        sampler2D _RustTex;
        sampler2D _SplatTex;

        struct Input
            float2 uv_CleanTex;

        half _Glossiness;
        half _Metallic;
        fixed4 _Color;

        // Add instancing support for this shader. You need to check 'Enable Instancing' on materials that use the shader.
        // See for more information about instancing.
        // #pragma instancing_options assumeuniformscaling
            // put more per-instance properties here

        void surf (Input IN, inout SurfaceOutputStandard o)
            fixed4 rustTex = tex2D(_CleanTex, IN.uv_CleanTex);
            fixed4 cleanTex = tex2D(_RustTex, IN.uv_CleanTex);
            fixed4 splatTex = tex2D(_SplatTex, IN.uv_CleanTex);

            o.Albedo = (rustTex.rgba * splatTex.r) + (cleanTex.rgba * (1.0 - splatTex.r));
    FallBack "Diffuse"


// Write black pixels onto the GameObject that is located
// by the script. The script is attached to the camera.
// Determine where the collider hits and modify the texture at that point.
// Note that the MeshCollider on the GameObject must have Convex turned off. This allows
// concave GameObjects to be included in collision in this example.
// Also to allow the texture to be updated by mouse button clicks it must have the Read/Write
// Enabled option set to true in its Advanced import settings.

using UnityEngine;
using System.Collections;

public class DotClick : MonoBehaviour
    public Camera cam;

    void Start()
        cam = GetComponent<Camera>();

    void Update()
        if (!Input.GetMouseButton(0))

        RaycastHit hit;
        if (!Physics.Raycast(cam.ScreenPointToRay(Input.mousePosition), out hit))

        Renderer rend = hit.transform.GetComponent<Renderer>();
        MeshCollider meshCollider = hit.collider as MeshCollider;

        if (meshCollider == null|| rend == null || rend.sharedMaterial == null || rend.material.GetTexture("_SplatTex") == null)

        Texture2D tex = rend.material.GetTexture("_SplatTex") as Texture2D;
        Vector2 pixelUV = hit.textureCoord;
        pixelUV.x *= tex.width;
        pixelUV.y *= tex.height;

        Circle(tex, (int)pixelUV.x, (int)pixelUV.y, 10,;
        // tex.SetPixel((int)pixelUV.x, (int)pixelUV.y,;

    // Taken from
    public void Circle(Texture2D tex, int cx, int cy, int radius, Color color)
        int x, y, px, nx, py, ny, d;

        for (x = 0; x <= radius; x++)
            d = (int)Mathf.Ceil(Mathf.Sqrt(radius * radius - x * x));

            for (y = 0; y <= d; y++)
                px = cx + x;
                nx = cx - x;
                py = cy + y;
                ny = cy - y;

                tex.SetPixel(px, py, color);
                tex.SetPixel(nx, py, color);

                tex.SetPixel(px, ny, color);
                tex.SetPixel(nx, ny, color);

Answered by Ruther Rendommeleigh on November 21, 2021

Add your own answers!

Related Questions

Ask a Question

Get help from others!

© 2021 All rights reserved.