- Home /
Detect light/shadow falling on object?
Is there a good performant way to detect light/shadow level of an object in real-time?
I'm thinking something along the lines of the "shadow meter" from the Thief games.
My first thought was that I might be able to simply sample a pixel from the texture of the object and then watch it for increasing or decreasing color (brighter/darker). However it seems that Texture2D.GetPixel() is not updated in real-time (the values are a constant after the game loads).
Is there someplace else I could be hooking into to take a really small sampling of changing colors due to changing light/shadow?
I also checked and it doesn't seem like $$anonymous$$aterial is updated in real-time, either (as far as GetColor is concerned). Perhaps the only way to do this would be some sort of custom Shader? Can Shaders send variables back and forth between scripts?
It seems like there might be some promise in using a custom shader, that can then pass a variable representing the amount of light, back to the script. I'm very new to the Shader/Cg stuff, though. It seems like the "primary" variable inside of the SetTexture call in a Vertex shader might be what I want...how do I go about getting that value back to a script, though? Set it as a property of the $$anonymous$$aterial I can check? How do I do that?
Was searching for similar solutions, maybe one idea could be: fake it. If you know where to shadows are, put "zones" or "hotspots" there, the nearer you are, the more "shadow points" you have. Or if there are dynamic objects moving & casting shadows, check on which side of the object you are and how far etc.
Answer by skovacs1 · Aug 27, 2010 at 04:22 PM
This is an interesting problem. I don't know how to get data out of a shader into a script directly, but that would be neat if it can be done - Based on the pipeline though, I don't think its possible.
What SetTexture Does
It would be great if there were a way to output a texture. While the documentation for SetTexture ("Assigns a texture") kind of doesn't convey this in and of itself, since a shader is a compiled program processed on the GPU, it doesn't exactly output textures back to the calling application. I'm not sure about all of the magic that ShaderLab is doing exactly, but in the end, it is still written within a shader which is compiled and sent to the GPU to process with a vertex list, input data and scene information (lighting, etc.), only to output pixels in an image. Obviously more shaders, vertices and data are sent at once in a single render call for the scene so that lighting and other things can be calculated.
To demonstrate, take a look at the built-in shaders: (This is from the Normal-Glossy built-in shader of Unity 2.6) SetTexture [_MainTex] {constantColor [_Color] Combine texture * primary DOUBLE, texture * constant}
. What this is doing is equivalent to saying at each point MainTex = Lighting or VertexColor 2 * Color. If this changed the MainTex that was stored in the material, then the mainTex would be changing constantly as it rendered, since the output texture is dependent upon the input texture. You will often see SetTexture [_MainTex] {combine texture}
which will tell it to combine the texture with the preceding and will trigger the Specular to be added when SeparateSpecular is on, but you don't see the textures in the materials changing.
Using RenderTexture (Pro Only) (Recommended)
This is the easiest and most correct approach. Because lighting is calculated by the GPU through the shader programs, the net result can't really be certain until it finsihes the render anyway.
I can think of a few resonable ways to go about this. Using renderTextures applied as texture 2D's, you could use the GetPixel to get the information you want. If you crop any secondary renders around your character, you could at least cut their cost and using shader replacement (everything is opaque black except the character who is white and does receive lighting), you could even more effectively cut that cost and the cost of checking against the render's results (grayscale images are easier to check than colour).
- Get the renderTexture of your main camera and check the color values where your character is. Performant since it doesn't require additional rendering unless you use shader replacement and even then, it only requires one additional render.
- Far more expensive, but you could render from the point of view of the lights and if they see your character, then the character is lit by the light. The amount they are lit is relative to the light's settings and the amount of the character visible. Can be optimized to only render from lights that could see the character. Not the most performant as it renders once for each light.
Using Raycasts (Limited)
You would need colliders on every shadow casting object. Cast a ray to your character from each light facing them and perform lighting calculation on your own. This will really only check against a single point on the character per raycast, so you should pick a point that is exposed on the character to check against. Performant enough with optimizations, few enough sufficiently spaced lights.
- First you would check if the light is facing the character and if not, continue to the next light - you would have to check fewer lights if you know they all have a range less than some value - This can be done using dot products.
- You then check the range of the light against the distance to your character.
- If your character is faced by the light and within the distance, raycast to the character from the light and if it hits, they are lit.
- You could then perform your own lighting calculation based on the shader - the math is actually pretty simple for the standard blinn and you don't really need to worry about specularity.
Using Shadow Volumes (probably slow and expensive)
You could generate your own shadow volumes, but each mesh would be assumed to be convex for simplicity. By calculating the vector from each light to each vertex within its lighting volume (cones, spheres, etc), you could define a series of hulls. Not very performant for scenes with any complexity as it has to iterate through every vertex in the scene.
- Every vertex whose normal facing the light defines the a face exposed to the light, with the most exterior of the vertices connected to these defining the perimeter of the face of the hull.
- The face of the hull is extended back into infinity along the vector to the light to define a shadow volume.
- Any vertex within a shadow volume is in shadow. Any vertex not within a shadow volue must be checked if it defines a shadow volume.
- Every vertex outside of the light volume is in shadow if no other light marks it as exposed to the light or in shadow.
- The intensity of the lighting would have to be specified on a per vertex level based on the light.
You could potentially speed this up by doing a check against representative objects (collider or bounding volumes of some sort) to see if an object is in shadow as checking against 8 vertices for a bounding box is a lot cheaper than checking against an entire mesh.
Answer by Stardog · Jul 25, 2013 at 07:14 AM
Edit: Updated for Unity 2018
Seems like you can use GetPixel() now. Here's a script with 2 different ways to get the Luma. One is fast performance, other a little slower. It will give a number between 0-1, 1 being the brightest.
Attach to the Player, and make the lightmaps readable by choosing Advanced in the Texture Import Settings and choose Read/Write. Choose your floors in the Layermask.
https://gist.github.com/st4rdog/fefe61884292ca457bbe9395c5458749
using UnityEngine;
using System.Collections;
public class LightmapPixelPicker : MonoBehaviour {
public Color surfaceColor;
public float brightness1; // http://stackoverflow.com/questions/596216/formula-to-determine-brightness-of-rgb-color
public float brightness2; // http://www.nbdtech.com/Blog/archive/2008/04/27/Calculating-the-Perceived-Brightness-of-a-Color.aspx
public LayerMask layerMask;
void Update()
{
Raycast();
// BRIGHTNESS APPROX
brightness1 = (surfaceColor.r + surfaceColor.r + surfaceColor.b + surfaceColor.g + surfaceColor.g + surfaceColor.g) / 6;
// BRIGHTNESS
brightness2 = Mathf.Sqrt((surfaceColor.r * surfaceColor.r * 0.2126f + surfaceColor.g * surfaceColor.g * 0.7152f + surfaceColor.b * surfaceColor.b * 0.0722f));
}
void OnGUI()
{
GUILayout.BeginArea(new Rect(10f, 10f, Screen.width, Screen.height));
GUILayout.Label("R = " + string.Format("{0:0.00}", surfaceColor.r));
GUILayout.Label("G = " + string.Format("{0:0.00}", surfaceColor.g));
GUILayout.Label("B = " + string.Format("{0:0.00}", surfaceColor.b));
GUILayout.Label("Brightness Approx = " + string.Format("{0:0.00}", brightness1));
GUILayout.Label("Brightness = " + string.Format("{0:0.00}", brightness2));
GUILayout.EndArea();
}
void Raycast()
{
// RAY TO PLAYER'S FEET
Ray ray = new Ray(transform.position, -Vector3.up);
Debug.DrawRay(ray.origin, ray.direction * 5f, Color.magenta);
RaycastHit hitInfo;
if (Physics.Raycast(ray, out hitInfo, 5f, layerMask))
{
// GET RENDERER OF OBJECT HIT
Renderer hitRenderer = hitInfo.collider.GetComponent<Renderer>();
// GET LIGHTMAP APPLIED TO OBJECT
LightmapData lightmapData = LightmapSettings.lightmaps[hitRenderer.lightmapIndex];
// STORE LIGHTMAP TEXTURE
Texture2D lightmapTex = lightmapData.lightmapColor;
// GET LIGHTMAP COORDINATE WHERE RAYCAST HITS
Vector2 pixelUV = hitInfo.lightmapCoord;
// GET COLOR AT THE LIGHTMAP COORDINATE
Color surfaceColor = lightmapTex.GetPixelBilinear(pixelUV.x, pixelUV.y);
// APPLY
this.surfaceColor = surfaceColor;
}
}
}
Hi, I've been trying to implement your script but I've run into an issue; line 50 throws the exception IndexOutOfRangeException: Array index is out of range
. I am pretty new to Unity though so I guess I've overlooked something..
This was for Unity 4.x (Beast lightmapper). Unity 5 uses a new one, so maybe it has some issues. I will look into it at some point.
It's also possible the hitInfo.collider.renderer shortcut doesn't work anymore, so try:
Renderer hitRenderer = hitInfo.collider.GetComponent<Renderer>();
Thanks a lot for your fast reply Stadog! I've already exchanged the method to assign the hitRenderer with the method you've suggested and it works fine. The array exception, however, seems to be thrown by the next line, the one with
'LightmapData lightmapData = LightmapSettings.lightmaps[hitRenderer.lightmapIndex];'
So I think the main issue is with the LightmapData. I guess the error is because I haven't fully baked the lightmaps so I'm planning to do some future research here if I find time. Anyways, It would be awesome, if you could also have a 2nd look on this script as well. An efficient and performant solution to this issue will be essential for any THIEF-like s$$anonymous$$lth game.
Was wondering if anyone figured this out? Tried it myself but I don't know enough of the inner workings to understand why it's not working. Was also wondering if maybe the detection could be turned to look at the object itself, rather than what it's resting on, to deter$$anonymous$$e its visibility. Thoughts?
I got it 'working' again. You have to change line 53 to 'lightmapData.lightmapColor'. There is no such thing as a lightmapNear in Unity 2017/18.
Remember to make read/write enabled on the lightmap textures next to your scene (turn off Auto Generate in Lighting).
The problem is that it seems that lightmapColor doesn't contain the same information as lightmapNear gave, so it's not giving the exact shade/brightness.
Here is the updated code - https://gist.github.com/st4rdog/fefe61884292ca457bbe9395c5458749
Thanks for your work Stardog!
I use your github code, but the "IndexOutOfRangeException: Array index is out of range. LightmapPixelPicker.Raycast () (at Assets/Resources/Textures/Skybox LightmapPixelPicker.cs:50) LightmapPixelPicker.Update () (at Assets/Resources/Textures/Skybox/LightmapPixelPicker.cs:13)" problem is still there. I updated the texture import-settings and Layer$$anonymous$$ask, but the color stays black and the values are both zero. I use 2017.1.1f1 Do you have any idea how to fix this?
This would only work with baked lightmaps - not with real-time ones. Shouldn't be to hard to change it to realtime lightmaps (hitRenderer.realtimeLightmapIndex) but still wouldn't handle lights that are not baked.
Answer by Indiecell · Jul 18, 2018 at 11:40 AM
Bump, Anyone get around the 'Array index is out of range' error?.
I've been looking round and it appears the ability to manually set array index for light map went out in the transition to unity 5?
Hope someone can help.
I got it 'working' again. You have to change line 53 to 'lightmapData.lightmapColor'. There is no such thing as a lightmapNear in Unity 2017/18.
Remember to make read/write enabled on the lightmap textures next to your scene (turn off Auto Generate in Lighting).
The problem is that it seems that lightmapColor doesn't contain the same information as lightmapNear gave, so it's not giving the exact shade/brightness.
Here is the updated code - https://gist.github.com/st4rdog/fefe61884292ca457bbe9395c5458749
Hi Stardog, thanks for the reply. I've rebuilt with the new line and your right that needed to be updated. This doesn't fix the ArrayIndex error though; or at least not when trying to use real time lighting.
In the earlier line where we grab the lightmap data, if we change: LightmapSettings.lightmaps[hitRenderer.lightmapIndex] To: LightmapSettings.lightmaps[hitRenderer.realtimeLightmapIndex]
The Lightmaps Array throws an error, I'm assu$$anonymous$$g because realtimeLight$$anonymous$$apIndex is not in the Lightmaps Array.
$$anonymous$$aybe there is a way we can edit the lightmaps Array?
I'm starting to get a little out of my depth here. Any help would be appreciated.
so do you have to bake the lights for this to work? curious cuz I just started working on a ninja game, and it wouldn't be very nija-y if you can't hide in shadows :)
Hi TheAmazingB74, currently yes this only works with baked lightmaps. For your style of game though i would follow the raycast route. The only reason I'm pushing so hard into this is because I'm working on a solar panel simulation where the amount of light is obviously really important. If your ninja being in the shadows is all you need to know then raycasts should be a good way to go. Good Luck!
Alrighty then!! Thanks for your reply. I appreciate the input