5
\$\begingroup\$

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.

\$\endgroup\$
4
\$\begingroup\$

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.

ClearableShader.shader:

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

        CGPROGRAM
        // 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 https://docs.unity3d.com/Manual/GPUInstancing.html for more information about instancing.
        // #pragma instancing_options assumeuniformscaling
        UNITY_INSTANCING_BUFFER_START(Props)
            // put more per-instance properties here
        UNITY_INSTANCING_BUFFER_END(Props)

        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));
        }
        ENDCG
    }
    FallBack "Diffuse"
}

DotClick.cs

// 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))
            return;

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

        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)
            return;

        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, Color.red);
        // tex.SetPixel((int)pixelUV.x, (int)pixelUV.y, Color.red);
        tex.Apply();
    }

    // Taken from https://answers.unity.com/questions/590469/drawing-a-solid-circle-onto-texture.html
    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);
            }
        }
    }
}
\$\endgroup\$
  • \$\begingroup\$ Hmm. This might be a bit too terse, but I hope it'll point you in the right direction. Feel free to ask followup questions and I'll expand the answer when I'm home. \$\endgroup\$ – Ruther Rendommeleigh Mar 12 at 11:43
  • \$\begingroup\$ Awesome, thanks for your reply. I'll take a crack at it and let you know what questions come up :) \$\endgroup\$ – Jason Mar 12 at 14:31
  • \$\begingroup\$ First stupid question, what do you mean by "okay with mesh colliders"? Do you mean as opposed to things like box colliders or rolling my own collision detection system? \$\endgroup\$ – Jason Mar 12 at 19:13
  • \$\begingroup\$ Mesh colliders, especially complex ones, can be noticeably slower than simple box, capsule or compound colliders. They're often more accurate, but if you use a large number of them, performance may suffer. \$\endgroup\$ – Ruther Rendommeleigh Mar 13 at 11:21
  • \$\begingroup\$ Gotcha, that's what I thought. I think I should be fine with mesh colliders because I envision the player mainly interacting with a small number of high detail items. One sword, one tool; good hitboxes will be paramount, so mesh colliders are (probably) the way to go. \$\endgroup\$ – Jason Mar 14 at 4:14

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.