0
\$\begingroup\$

I think what I want to achieve is similar to how grass can get the terrain color so it looks more blended, though this is not what I will be doing.

My texture is a palette of colors:

I have created a basic example in Unity. The cube represents an object that could be placed (and moved again later). The plane with the colors uv mapped would act as the ground (this will become a more complex mesh).enter image description here

How can I sample the color and apply it to the object placed?

Edit:

If the object is over more than one color, then ideally I would like to blend those colors instead of having a hard transition.

\$\endgroup\$
2
  • \$\begingroup\$ What happens if the cube is sitting on the corner between red, green, and blue? Should all three colours appear on the cube, or should the whole cube be one colour - maybe a weighted blend of all three, or whichever single colour is closest to the middle of the cube, or under the most of it? \$\endgroup\$
    – DMGregory
    Sep 27, 2020 at 19:16
  • \$\begingroup\$ All one color would be fine. Am not too bothered about it being super accurate.Edit: Weighted blend actually sounds good as well. Though for now I want to try and learn the basics and then maybe I can improve on it as I go. \$\endgroup\$ Sep 27, 2020 at 19:28

1 Answer 1

2
\$\begingroup\$

I wrote this answer based on your initial reply "All one colour would be fine" - so it doesn't handle weighted blending. If you want that, it's doable, but would take more work.

Animated example of colour-changing cube

The basic strategy goes like this:

  • Each frame in Update, we fire a ray to see what part of the ground we're over.

  • We read the texture coordinate of the point the ray hit to figure out which part of the palette texture is displayed there.

  • We apply that texture offset to our own renderer's material that's using that same colour palette texture, so we sample the same colour as the point we hit.

This sample is written for the built-in render pipeline. If you're using the URP/HDRP or a custom scriptable render pipeline, you'd likely want to do away with material property blocks and instead create a copy of the material and set the UV offset there directly - the SRP batcher handles that more efficiently than the built-in pipeline's property blocks.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

[RequireComponent(typeof(Renderer))]
public class ColourMatcher : MonoBehaviour {
    [Tooltip("What physics layers should our raycast check for colour sources?")]
    public LayerMask colourSource;
    
    Renderer _renderer;
    static MaterialPropertyBlock _materialProperties;
    static int _textureScaleOffsetID = Shader.PropertyToID("_MainTex_ST");

    void Start() {
        _renderer = GetComponent<Renderer>();
        if(_materialProperties == null)
            _materialProperties = new MaterialPropertyBlock();
    }

    void Update() {
        // Create a ray fired downward from our center.
        var ray = new Ray(transform.position, Vector3.down);

        // Check to see what the ray hits.
        if (Physics.Raycast(ray, out RaycastHit hit, colourSource)) {
            // If we hit something, take the UV coordinate of the hit,
            // and apply it as our texture offset.

            // The x and y components of the ST vector are the scale.
            // I set these to zero so all parts of the cube sample the same point.
            Vector4 scaleTranslate = Vector4.zero;

            // The z and w components of the ST vector are the translation.
            // I set these to match the UV of the part of the texture that we hit.
            scaleTranslate.z = hit.textureCoord.x;
            scaleTranslate.w = hit.textureCoord.y;

            // Set our renderer to use this texture scale and offset,
            // rather than whatever the default for the material was.
            _materialProperties.SetVector(_textureScaleOffsetID, scaleTranslate);
            _renderer.SetPropertyBlock(_materialProperties);
        } else {
            // If our raycast hits nothing, by default we'll just keep our previous colour.
            // But you could modify this to switch to a default colour instead here. 
        }
    }
}

To extend this to do a weighted blend of colours, we would...

  • Fire multiple rays around the cube to sample multiple points (the more points, the smoother the blend and the more sensitively we can catch small corners of a different colour)

  • For each UV coordinate we find, look up the corresponding pixel from the texture (which now needs to be marked CPU readable) to find the colour

  • Average the colours from all our hits

  • Blend our material's current _Color property toward this target, to have it fade smoothly instead of snapping.

Or, an alternative:

  • Set up a second camera that looks straight down, orthographically capturing our ground geometry. It should use a layer mask to ignore any other visual objects

  • Output what that camera sees into a RenderTexture

  • Use that RenderTexture in our cube's material

  • In the cube's shader, find the position of the center of the cube in the second camera's field of view (you can use a script to save its inverse view matrix as a shader variable)

  • Sample the RenderTexture at that point with bilinear filtering, or average samples from several nearby samples, to get the average colour the camera sees there.

You may need to use shader replacement with this strategy to exclude the effects of lighting, which could tint what gets captured into the RenderTexture if you're not using unlit materials.

\$\endgroup\$

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .