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
}
}
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\$