_WorldSpaceLightPos0, how it works?

Discussion in 'Shaders' started by georgeq, Oct 10, 2016.

1. I'm writing a simple shader that makes objects to emit light depending on the distance from the light source. Because of the variable's name, I'm assuming it contains the position of the main directional light in world space, and if I understand well, I can retrieve the vertex world position simply multiplying the vertex by the UNITY_MATRIX_MVP, like this:

float3 vertexw = mul(UNITY_MATRIX_MVP, v.vertex).xyz;

and to obtain the distance between both I can do this:

float dist = distance(vertexw,_WorldSpaceLightPos0.xyz);

In my scene all objects are located between 300 and 1700 meters away from the directional light, so if I make the light emission equal to: (dist - 300) / 1400 then I should get a number between 0 and 1... and I do, however it seems that I'm getting the distance form the camera and not from the light source (as the name of the variable indicates), because the light emitted by the object varies depending on how close the camera is to the object and the position of the directional light seems irrelevant in this case.

Here is the full code:

Properties{
_Color("Color",Color) = (1,1,1,1)
_MainTex("Texture", 2D) = "white" {}
}

Tags{ "RenderType" = "Opaque" }
//Tags{"LightMode" = "ForwardBase"}

CGPROGRAM
#pragma surface surf Standard vertex:vert
struct Input {
float2 uv_MainTex;
float emission;
};
float4 _Color;
sampler2D _MainTex;

void vert(inout appdata_full v, out Input o) {
UNITY_INITIALIZE_OUTPUT(Input,o);
float3 vertexw = mul(UNITY_MATRIX_MVP, v.vertex).xyz;
float dist = distance(vertexw,_WorldSpaceLightPos0);
o.emission = (dist - 300) / 1400;
}

void surf(Input IN, inout SurfaceOutputStandard o) {
o.Albedo = float4(0, 0, 0, 1);//tex2D(_MainTex, IN.uv_MainTex).rgb*_Color;
o.Emission = float4(1,1,1,1)*IN.emission;
}
ENDCG
}

Fallback "Diffuse"

}

notice the Tags line that specifies the light model is commented because otherwise the shader renders a solid black object.

Tanks for reading, hope you can help me out.

2. Directional lights are just directional, shaders have no knowledge of their position in the world, only their direction. That means the kind of data that's stored in _WorldSpaceLightPos0 changes depending on the light type.

If the w component is 1, it's a spot or point light world space position and you should do distance falloff. If it's 0 then it's a directional light world space direction.

Also if "LightMode"="ForwardBase" is true then the _WorldSpaceLightPos0 is always for a directional light, even if none exists in the scene. Only "LightMode"="ForwardAdd" has support for passing the position of point lights.

3. There's been a lot of time since I read about lighting, I didn't recall that, but you are right directional light's position is not relevant for lighting calculations, only it's direction... In fact, after some testing I discovered that _WorldSpaceLightPos0 is indeed a normalized vector.

Thanks!

4. ifurkend

Joined:
Sep 4, 2012
Posts:
350
Sorry for digging the grave, but I really have trouble to figure out how _WorldSpaceLightPos0 translates from the rotation of the directional light. After some laborious testing myself, _WorldSpaceLightPos0 treats the angle from 90-180-270 degree backward, IOW reflected as 90-0-270 degree.

What I am trying to achieve is to use _WorldSpaceLightPos0 to calculate the d.light rotation projected on the camera to rotate texture on the material which will be used on billboard particles.

Last edited: May 1, 2017
5. There's nothing wrong with _WorldSpaceLightPos0, the light direction would be completely broken if it was.

What you're seeing is a textbook example of an improperly constructed rotation matrix, usually from using a single calculated cosine or sine and extrapolating the other rather than calculating them both.

6. ifurkend

Joined:
Sep 4, 2012
Posts:
350
So any tutorial points to the solution? The rotation in my shader works when it uses a single angle from material property rather than _WorldSpaceLightPos0. I just can't find any explanation on Unity website that what _WorldSpaceLightPos0 actually outputs. It's just not the straightforward (x,y,z) rotation of the d.light.

Last edited: May 2, 2017
7. For directional lights it's just a unit length direction vector, for point lights it's exactly what the name says it is, the world space position of that light. That's also what the documentation says it is.

8. ifurkend

Joined:
Sep 4, 2012
Posts:
350
I checked this already before asking, but it seems to me it's the following:

_WorldSpaceLightPos0.x = cos(dLight.eulerangles.x) * sin(dLight.eulerangles.y) * -1
_WorldSpaceLightPos0.y = sin(dLight.eulerangles.x)
_WorldSpaceLightPos0.z = cos(dLight.eulerangles.x) * cos(dLight.eulerangles.y) * -1

I may be wrong totally, but Built-in variables is just so bare that it doesn't explain the whole story.

9. I mean, it could be equivalent, but Unity doesn't actually store rotations as euler angles, those are just for displaying back to the user and for ease of use. The rotation is stored internally as a quaternion, so the value passed to the shader is likely just:
light.transform.rotation * Vector3.forward

Where the transform.rotation is the object's quaternion rotation. How the vector is generated is kind of irrelevant though, you're trying to extract euler angles or at least a rotation matrix from just a unit length directional vector. You can create that vector with trig, like your example code, or matrix / quaternion rotations, which are usually cheaper. There's no way to know what method was used from the resulting vector, and attempting to extract euler angles from that vector is going to be just a guess.

Back to the main issue of rotating the texture, try this:
float2 lightVec = normalize(_WorldSpaceLightPos0.xy);
float2x2 lightVecRotationMatrix(lightVec.x, -lightVec.y, lightVec.y, lightVec.x);
float2 rotatedUV = mul(uv, lightVecRotationMatrix);

I think I have that right, though the lightVec.x and lightVec.y might be backwards, or you might need to negate the light direction.

ifurkend likes this.
10. ifurkend

Joined:
Sep 4, 2012
Posts:
350
Yes, it works. But without Euler rotation, I have lost my way in changing the rotation offset from bottom-left corner to the center of the uv because it's still relative to the d.light rotation. This is how I do it with Euler rotation:
i.uv.x += _radius * sin (_angle+0.78539816339744830961566084581988) - 0.5; // 0.785 = 45deg * pi/180;
i.uv.y += _radius * cos (_angle+0.78539816339744830961566084581988) - 0.5;

Do I need to study quaternion to get the basic translation and rotation right in the shader script? It seems nearly unavoidable in order to calculate the projection of d.light rotation on viewDir...

Just found out that I can do another mul(UNITY_MATRIX_V,uvRotated), even though again, lot of guess work to get my desired result...

Last edited: May 2, 2017
11. Nothing in your code has anything to do with Euler btw. Euler rotation is using Pitch, Yaw, & Roll angles to define an orientation, and those angles can be in radians or degrees. Talking about quaternions or Euler angles here is all a red herring and has nothing to do with the shader code we're working with which is purely vectors and matrix transform math. You're just constructing a rotation matrix with trigonometry from a single angle.

So just adjust the center position pre and post rotation:
float2 lightVec = normalize(_WorldSpaceLightPos0.xy);
float2x2 lightVecRotationMatrix(lightVec.x, -lightVec.y, lightVec.y, lightVec.x);
float2 rotatedUV = mul(uv - 0.5, lightVecRotationMatrix) + 0.5;

The real problem is the light direction is in world space, and you need it to be in tangent (ie: UV) space so you know how to rotate the texture. For camera facing & aligned square billboards, tangent space and view space are going to be the same, so you're probably getting something that feels a little closer to "correct" by applying the view rotation, but you're applying a 3D transform matrix to a 2D position which is going to result in some weirdness.

A closer version would be:
float3 viewLightDir = mul((float3x3)UNITY_MATRIX_V, _WorldSpaceLightPos0.xyz);
float2 lightVec = normalize(viewLightDir.xy);
float2x2 lightVecRotationMatrix(lightVec.x, -lightVec.y, lightVec.y, lightVec.x);
float2 rotatedUV = mul(uv - 0.5, lightVecRotationMatrix) + 0.5;

But this is still technically wrong, though it might work fine for your case.

More correct version would be:
Code (CSharp):
1. // Calculate the world normal, tangent, and binormal
2. float3 worldNormal = UnityObjectToWorldNormal(v.normal);
3. float3 worldTangent = UnityObjectToWorldDir(v.tangent.xyz);
4. float tangentSign = v.tangent.w * unity_WorldTransformParams.w;
5. float3 worldBinormal = cross(worldNormal, worldTangent) * tangentSign;
6.
7. // Construct a world to tangent rotation matrix
8. float3x3 worldToTangent = float3x3(worldTangent, worldBinormal, worldNormal);
9.
10. // Rotate world space light direction
11. float3 tangentLightDir = mul(tangentToWorld, _WorldSpaceLightPos0.xyz);
12.
13. // Apply UV space rotation
14. float2 lightVec = normalize(tangentLightDir.xy);
15. float2x2 lightVecRotationMatrix(lightVec.x, -lightVec.y, lightVec.y, lightVec.x);
16. float2 rotatedUV = mul(uv - 0.5, lightVecRotationMatrix) + 0.5;
It's totally avoidable since you're not doing anything with quaternions. This is all matrix math... which isn't avoidable.

ifurkend likes this.
12. ifurkend

Joined:
Sep 4, 2012
Posts:
350
@bgolus You're totally right about my intent and that UNITY_MATRIX_V doesn't do too much for me. I don't know how much I want to ask you but obviously I have attempted to run before learning to crawl.

Maybe you have realized it already, this whole thing is to recreate the volumetric shadow on billboard particles. AFAIC the earliest use of this technique in video game was in Shadow of the Colossus (2005) whose developers published a technical note in Japanese.

Some change needs to be done in custom vertex streams of particle system. Again I have absolutely no idea how the "UV2 (texcoord0.zw)" part makes sense at all. One sad thing is that the gradient mask doesn't work properly when using texture sheet animation (either for animation or random frame for variety) because this is one special case that the module is not expected to handled (@richardkettlewell). Code (CSharp):
1. /*
2. bgolus and ifurkend (official Unity forum).
3. Real-time directional light is required for this shader to work.
4. Otherwise use the plain Particle Alpha Blended shader and
5. merge the main texture and gradient map in graphic editor beforehand.
6. */
7. Shader "Particles/Alpha Blended Amb Dir Lit"
8. {
9.    Properties
10.    {
11.        _MainTex ("Texture", 2D) = "white" {}
13.        _GradPow ("Gradient Power", Range(0,2)) = 1 //This line can be removed if you have no intention to animate gradient power.
14.        _AmbientPow ("Ambient Power", Range(0,1)) = 0.5 // Can be used in HDR effect. Intensity greater than 2 causes glitch in HDR rendering.
15.        _Glow ("Intensity", Range(0,10)) = 1
16.    }
18.    {
19.        Tags { "Queue" = "Transparent" "IgnoreProjector" = "True" "RenderType" = "Transparent" "LightMode" = "ForwardBase" "PreviewType" = "Plane"}
20.        LOD 100
21.        Cull Off
22.        ZWrite Off
23.        Blend SrcAlpha OneMinusSrcAlpha
24.
25.        Pass
26.        {
27.            CGPROGRAM
28.            #pragma vertex vert
29.            #pragma fragment frag
30.
31.            #include "UnityCG.cginc"
32.            #include "UnityLightingCommon.cginc"
33.
34.            struct appdata
35.            {
36.                float4 vertex : POSITION;
37.                float2 texcoord : TEXCOORD0;
38.                float4 normal : NORMAL;
39.                float4 tangent : TANGENT;
40.                fixed4 color : COLOR;
41.                UNITY_VERTEX_INPUT_INSTANCE_ID
42.            };
43.
44.            struct v2f
45.            {
46.                float4 vertex : SV_POSITION;
47.                float4 uv : TEXCOORD0;
48.                fixed4 color : COLOR;
49.                UNITY_VERTEX_OUTPUT_STEREO
50.            };
51.
52.            sampler2D _MainTex;
54.            float4 _MainTex_ST;
56.            half _AmbientPow;
57.            half _Glow;
58.
59.            v2f vert (appdata v)
60.            {
61.                v2f o;
62.                o.vertex = UnityObjectToClipPos(v.vertex);
63.                o.uv.xy = TRANSFORM_TEX(v.texcoord, _MainTex);
64.
65.                // Calculate the world normal, tangent, and binormal
66.                float3 worldTangent = UnityObjectToWorldDir(v.tangent.xyz);
67.                float tangentSign = v.tangent.w * unity_WorldTransformParams.w;
68.                half3 worldNormal = UnityObjectToWorldNormal(v.normal);
69.                float3 worldBinormal = cross(worldNormal, worldTangent) * tangentSign;
70.                // Construct a world to tangent rotation matrix
71.                float3x3 worldToTangent = float3x3(worldTangent, worldBinormal, worldNormal);
72.
73.                // Rotate world space light direction
74.                float3 tangentLightDir = mul(worldToTangent, _WorldSpaceLightPos0.xyz);
75.
76.                // Apply UV space rotation
77.                float2 lightVec = normalize(tangentLightDir.xy);
78.                float2x2 lightVecRotationMatrix = float2x2(lightVec.x, -lightVec.y, lightVec.y, lightVec.x);
79.                o.uv.zw = mul(mul (o.uv.xy-0.5, lightVecRotationMatrix),float2x2(0,1,-1,0))+0.5;
80.
81.                o.color = v.color * float4(_LightColor0.rgb,1) * _Glow;
82.                o.color.rgb += ShadeSH9(half4(worldNormal,1)) * _AmbientPow;
83.                return o;
84.            }
85.
86.            fixed4 frag (v2f i) : SV_Target
87.            {
88.                fixed4 col = tex2D(_MainTex, i.uv.xy);
89.                fixed4 col2 = tex2D(_GradMap, i.uv.zw);
90.                col2.rgb = 1+(saturate(i.color.a*1.6)*_GradPow*(col2.rgb-1)); //This line can be removed if you have no intention to animate gradient power, but the gradient should fade sooner as opacity drops because of reduced thickness.
91.                col *= col2 * i.color;
92.                return col;
93.            }
94.            ENDCG
95.        }
96.    }
97.    FallBack "Mobile/Particles/Alpha Blended"
98. }

Last edited: May 4, 2017
13. I figured something like this was what you were working on. Btw, most of those UV calculations should be done in the vertex shader so you only have to pass the color and two UV sets (packed in a single float4) instead of passing the normal and tangent and doing the (relatively expensive) matrix math in the fragment shader.

Also they didn't use this technique in Shadow of the Colossus, they just used faked vertex lighting, but the images they released showing the technique people mistakenly took as being done with two textures. Sampling multiple textures in a single pass isn't possible on the PS2 as it didn't support multi-texturing. Every effect with more than one texture read had to be done with multiple passes and blend operations.

Even normal mapping required the character to be rendered 4 times (just for the normal mapped lighting, you needed a fifth to apply the diffuse color). Motion blur was done by rendering the framebuffer on multiple transparent planes!

ifurkend likes this.
14. ifurkend

Joined:
Sep 4, 2012
Posts:
350
I modified the shader again to keep the fragment shader as simple as possible. If matrix multiplication isn't cheap... does that mean this shader isn't cheap compared to the basic bumped diffuse shader?

I had no idea... even in Unity vertex lit billboard particle is so difficult to get the desired lighting result I would just pretend it didn't exist...

15. Bump mapping is usually a little slower, because it requires a lot of the same matrix multiplications to do 3d rotations. In fact that bit of code for constructing the world to tangent matrix is taken from Unity's own shaders used to construct their tangent to world matrix that they apply to the normal map in the fragment shader. More over the UnityObjectToWorldDir() and UnityObjectToWorldNormal() functions are just wrappers for 3d matrix multiplication rotations as well.

Of course, you also don't get the benefit of per-pixel normal mapping.

ifurkend likes this.
16. Tutanhomon

Joined:
Sep 15, 2009
Posts:
101
You could use light vector to paint particle vertices, and then just use vertex colors instead gradient texture.
Anyway, your approach is really great! unityunity