# Changing vertex colors based on mesh proximity?

Would it be possible to create a shader that changes the vertex colors of an object based on the proximity of another object?

For example :

A terrain object that is all green, but when you move a sphere over a part of the terrain, the terrains underlying vertices turn red?

I would like to do this so that I can have a terrain that is textured through vertex colors, and then change the underlying terrain texture under a specific object when that object is plopped onto the terrain. (think of a house placed on a green meadow that âstamps outâ the green grass and turns it into dirt)

1 Like

Shaders canât really access the world positions of different objects in the world âout of the boxâ. They only know about their own world position. We can kinda simulate it, though. Itâs actually quite easy to do too.

Since I donât know your level of experience, Iâm going to assume you know jack about shaders and shader math, so Iâll explain everything in detail. Prepare for a wall of text my friend.

Ok, letâs do this.

• First, the shader itself. See the images below for the finished graph, in case you have difficulty following along, or if donât care why this works and just want the solution already.
• Create a shader graph with six properties: SperePos (Vector3), SphereRadius (float), MinDistance (float), MaxDistance (float), TextureA (texture2D) and TextureB (texture2D). Donât forget to name the property references to â_SpherePosâ, â_SphereRadiusâ, etc., or else setting the SpherePos and SphereRadius properties from a C# script isnât going to work.
• Now, letâs place our nodes. First, we must calculate the distance between our pixel and the point on the sphereâs surface that is the closest to our pixel. This is easier than it sounds. We subtract the SpherePos from our world-space position (which gets us the position of the sphere relative to our pixel), make it absolute (because the relative position can be either positive or negative, and we want the positive value), get the length of that vector (which gets us the distance to the center of the sphere), and subtract the SphereRadius (which gets us the distance to the sphereâs surface). Lastly we clamp this value between MinDistance and MaxDistance.
• Next, we want to convert this value into a point on the range 0 to 1, so that we can use it in a Lerp operation (which is how we interpolate between our two textures). If our current distance == MinDistance, we want the value 0, and if our current distance == MaxDistance, we want the value 1. Achieving this is super easy, we just run the math (distance - MinDistance) / (MaxDistance - MinDistance), which looks way more complicated with graph nodes than it really is.
• Now the interpolation part. We sample both our textures, and pipe the result into a Lerp node. The result from the previous step is used as the âTâ input. So if our T input value is 0, then TextureA will be fully shown. If it is 1, then TextureB will be fully shown. If it is somewhere in-between, then the two will be blended accordingly.
• Pipe the result of the Lerp node into your fragment node.
• Create a material and set your textureA, textureB, MinDistance and MaxDistance properties and bam youâre done with this part.

Now we need to make sure that the SpherePos and SphereRadius properties are set. You can use the folllowing script for that. Itâs super simple, and just sets the two properties in the Update function, using a material property block. If you donât know what that is, just know that it can set material properties for one renderer only (see the documentation page for more info). Note that it expects the sphere object to have a SphereCollider, which is where it gets the radius from (instead of somehow calculating it from the mesh itself, which would be insane).

``````using UnityEngine;

public class SphereTracker : MonoBehaviour
{
public Transform sphere;
public new Renderer renderer;

private MaterialPropertyBlock propBlock;
private SphereCollider sphereCol;

private void Awake()
{
propBlock = new MaterialPropertyBlock();
sphereCol = sphere.GetComponent<SphereCollider>();
}

private void Update()
{
propBlock.SetVector("_SpherePos", sphere.position);
renderer.SetPropertyBlock(propBlock);
}
}
``````

Thatâll do, methinks. Ask away if you still have questions.

2 Likes

Thank

Thank you for that incredible answer! It has definitely put me on the right pathâŚ

I modified the script and shader a bit to support a box collider rather than a sphere. The goal would be to have a bit more of a grid look to the way the terrain gets textured.

``````using UnityEngine;

public class DecoTracker : MonoBehaviour
{
public Transform deco;
public new Renderer renderer;

private MaterialPropertyBlock propBlock;
private BoxCollider boxCol;

private void Awake()
{
propBlock = new MaterialPropertyBlock();
boxCol = deco.GetComponent<BoxCollider>();
}

private void Update()
{
propBlock.SetVector("_DecoPosition", deco.position);
propBlock.SetVector("_DecoSize", boxCol.size);
renderer.SetPropertyBlock(propBlock);
}
}
``````

However, even before I made my changes, I wasnât getting any live updates to the Position value of the material. It was just 0,0,0,0. Now that I have changed to a boxcollider, the scale of that box collider also stays at 0,0,0,0. It does not seem to be grabbing these values from my âstampâ object.

I can think of a few possible reasons that might be causing this issue maybe.

1: Changes done by the C# script will not be visible on the Material asset itself, because Material Property Blocks only change the local instance of a Material attached to a renderer. If you select the rendererâs game object itself while the game is running, the bottom of the inspector will show the instance of the Material with the MPB applied to it.

2: Make sure the shader propertyâs âReferenceâ entries are set to â_DecoPositionâ and â_DecoSizeâ. This is not the same as the propertyâs name (which is what is shown in the inspector if you select a Material asset), but is instead an internal handler that is used to reference the property through code (itâs also what is used in shader code that your graph will compile into). By default, Unity generates really long references that are not similar to the property names at all. You donât want to be using those.

3: Itâs also possible that youâre not re-applying the Material Property Block to the renderer every Update. If you donât do this, then the rendererâs material instance wonât reflect any changes made to it. This doesnât seem to be the case, judging by the script you posted, but I thought Iâd mention it regardless.

Hope one of those will do the trick.

As for the grid effect that you want to achieve, using a box collider isnât going to work (unless you made heavy modifications to the graph), because my math was designed for spherical Deco objects. If you use a box, the step where you subtract the radius will break, because boxes donât have a radius. Essentially, what youâd be doing is subtracting the boxâs size, which is a vector, from a float representing the distance to the center of the box. Unity will then turn that float into a Vector2. So youâd end up with an output of `Vector2(distance - box.size.x, distance - box.size.y)`, when the output of this step should be a float. That doesnât really make sense, and stuff just gets kinda weird from there on out.

Instead, what weâre going to do is divide up the world along a 3D grid, determine in which grid space a pixel is, and perform the distance calculation to the sphere from the center of that grid space, instead of from the pixel itself. This will ensure that the lerping factor is the same for every pixel in a grid space. Youâll need an extra float shader property for this, which will represent the width, height and depth of a grid space. Something like the graph below will calculate the grid indices of a pixel, and convert that to a world coordinate.

Plug that bad boy into the start of the earlier graph, and youâll get something like the picture below.