1
\$\begingroup\$

I'm simulating laser scanners in Unity with Raycasts, and I've got some "bumpy" items I'd like to scan.

I had tried generating the actual geometry for these objects, but there are tons of these objects and the polygon count exploded.

What I would like to do is something like described here to get the color from a raycast intersection:

Renderer renderer = raycastHit.collider.GetComponent<MeshRenderer>();
Texture2D texture2D = renderer.material.mainTexture as Texture2D;
Vector2 pCoord = raycastHit.textureCoord;
pCoord.x *= texture2D.width;
pCoord.y *= texture2D.height;

Vector2 tiling = renderer.material.mainTextureScale;
Color color = texture2D.GetPixel(Mathf.FloorToInt(pCoord.x * tiling.x) , Mathf.FloorToInt(pCoord.y * tiling.y));

But, instead of getting the color, I'd like to access the normal. I used grayscale bump maps to simulate height differences, but the raycasting doesn't take the normal map into account and I can't figure out how to access normal map values. Ideally there'd be some texture2D.GetNormal() I could use in place of the texture2D.GetPixel() given in the snippet above, but I can't seem to find anything anywhere on getting at the material normals.

I'm using Unity 2019.4, with the Universal Render Pipeline at the moment.

\$\endgroup\$
2
  • \$\begingroup\$ You can't ask mainTexture.GetNormal() because the main/albedo/colour texture doesn't store normals in the first place. That's the normal map's job. So you don't need a new .GetNormal() method here, you just need to .GetPixel() from the texture that contains your normal map, eg fetched using material.GetTexture(). \$\endgroup\$
    – DMGregory
    Commented Jul 6, 2020 at 21:48
  • \$\begingroup\$ @DMGregory - excellent advice! Not at my laptop right now but I'll check it first thing tomorrow morning. I think there's just the .material and .materials properties, though - i.e., no .diffuse or .normal properties to access. I'm doing over 100k raycasts per second and don't want to use a string compare against all the possible normal maps. Is the normal texture always in the same spot in the materials array? I'll be able to check more myself in the morning but was hoping you might know offhand. Anyways thanks for the advice! I'll accept it if you can please make it an answer :) \$\endgroup\$
    – Chuck
    Commented Jul 6, 2020 at 23:31

1 Answer 1

1
\$\begingroup\$

Following @DMGregory's excellent feedback, I wrote the following class to test:

public class MaterialTextureTest : MonoBehaviour
{
    public Material material = null;
    public List<string> textureNames = new List<string>();
    public List<Texture2D> textures = new List<Texture2D>();
    // Start is called before the first frame update
    void Start()
    {
        if(material!=null)
        {
            string[] names = material.GetTexturePropertyNames();
            foreach(string name in names)
            {
                textureNames.Add(name);
                textures.Add((Texture2D)material.GetTexture(name));
            }
        }
    }
}

This showed the material has the following texture property names:

_MainTex
_AlphaTex
_BumpMap
_NormalMap
_DetailNormalMap
_BaseMap
_MetallicGlossMap
_ParallaxMap
_OcclusionMap
_EmissionMap
_DetailMask
_DetailAlbedoMap
_SpecGlossMap
_MaskTex

I'm using the Universal Render Pipeline and my test material is using the "Simple Lit" shader. I've got a texture put into the "Normal Map" section of the "Surface Inputs" portion of the material, but I actually drew a grayscale image to use for the normal map, with the intention of using it as a true bump map.

I believe this is handled on the texture, because there's a checkbox on the texture properties that says "Create from Grayscale" that I ticked.

I used the grayscale/bump map texture as the input to the "Normal Map" setting for my material, so the elements in my textures List from the test class above are:

Element 0    <My image texture>
Element 1    None (Texture 2D)
Element 2    <My bump map texture!>
Element 3    None (Texture 2D)
Element 4    None (Texture 2D)
Element 5    <My image texture>
Element 6    None (Texture 2D)
Element 7    None (Texture 2D)
Element 8    None (Texture 2D)
Element 9    None (Texture 2D)
Element 10   None (Texture 2D)
Element 11   None (Texture 2D)
Element 12   None (Texture 2D)
Element 13   None (Texture 2D)

So now I can see that I can access my bump map by calling:

Texture2D bumpMap = (Texture2D)material.GetTexture("_BumpMap");

Then finally I just use this line instead of the:

Texture2D texture2D = renderer.material.mainTexture as Texture2D;

line in the snippet from my question to access the bump map, just like DMGregory said. Now the "color" (grayscale) I get back from the .GetPixel() line returns the grayscale value I wrote.

I think a super noteworthy point here is that most "bump" maps are actually normal maps, which are the blue/purple looking maps like CrazyBump or similar would generate. I'm trying to encode a height offset on the objects to use for modifying the Raycast results, so there's this double benefit for me in that the grayscale bump maps seem to actually correlate better to a height than the surface normals, but also that the actual grayscale bump maps are rarely used, so it's like this hidden channel included in all materials.

For my test material, with a grayscale bump map, the _NormalMap entry in the material is null! I am expecting the opposite to be true for most standard materials - the _BumpMap entry will probably be null because most modern materials will be using an actual normal map.

All that is to say I can try to get the _BumpMap texture and check if it's null. If it's null it's probably not a material I've made, which means I skip the raycast modification and use the actual results. If it's not null, then I can get the color as described above and use that to modify the results.

:EDIT:

I had tried this implementation and found it was still too slow for what I need, so I used a dictionary! This is my first time really needing one, but I found that the .GetComponent<Renderer>() was the bottleneck in the snippet from my question.

This part isn't really related to the question per se, but it's related to what I'm doing, so I figured I'd post my solution here for all the future visitors. I can skip finding the renderer, then finding the material, then finding the texture every time and just associate a texture directly with the gameObject by caching them in a Dictionary. Dictionaries are built for quick lookups and I found the following to be very, very fast (virtually no change in framerate versus without any of the texture lookup code now):

// In the class definition:
private Dictionary<GameObject, Texture2D> textureDictionary = new Dictionary<GameObject, Texture2D>();
Renderer renderer;
Material material;
Texture2D texture;
Vector2 textureCoord;
Color color;
int textureId = Shader.PropertyToID("_BumpMap");


// After running the raycasts:
// If the texture isn't in the dictionary, add it!
if (textureDictionary.TryGetValue(results[i].collider.gameObject, out texture) == false)
{
    bool textureFound = false;
    renderer = results[i].collider.gameObject.GetComponent<Renderer>();
    if (renderer != null) // Terrain has no renderer, so need to check
    {
        material = renderer.sharedMaterial;
        texture = (Texture2D)material.GetTexture(textureId);
        if (texture != null) // We're looking for the bump map texture here, which could be null
        {
            textureDictionary.Add(results[i].collider.gameObject, texture);
            textureFound = true;
        }
    }
    if(textureFound == false) // If the texture doesn't exist that's fine, but put a null entry in the dictionary so you don't continue to look for it every iteration
    {
        texture = null;
        textureDictionary.Add(results[i].collider.gameObject, texture);
    }
}
if(texture!=null)
{ 
    textureCoord = results[i].textureCoord;
    color = texture.GetPixelBilinear(textureCoord.x, textureCoord.y);
    // Do your stuff with the bump map color now
}

}

\$\endgroup\$
3
  • \$\begingroup\$ So I just got done testing everything, and was pumping 100k points per second as previously mentioned. I think the string compare is a performance killer here, but whatever the case my framerate spikes on raycasting went from about 50fps without the texture lookups to about 15 fps with the texture lookups. It's too much of a performance hit for me to use, but was good practice to work with something I don't normally touch. \$\endgroup\$
    – Chuck
    Commented Jul 7, 2020 at 15:34
  • \$\begingroup\$ Note that you can skip the string comparisons by caching a Shader.PropertyToID integer to use instead. You may also want to use renderer.sharedMaterial to avoid copying the material when all you want to do is read from it (which also breaks batching). \$\endgroup\$
    – DMGregory
    Commented Jul 7, 2020 at 16:10
  • \$\begingroup\$ @DMGregory - Thanks for all your help! I found your suggestions helped, but still not enough, so I used a dictionary to do my texture lookups. Framerate in the profiler is averaging around 1000 fps without the laser scanner, and then the scanner causes spikes up to about 80-90 fps on scan events, which is good enough for me on a laptop. \$\endgroup\$
    – Chuck
    Commented Jul 7, 2020 at 20:09

You must log in to answer this question.

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