Question - Dynamic Lighting/Shadows

SongOfTheWeave

16-02-2008 05:54:10

I'm working on adding dynamic lighting/shadows for Movable Objects and such on top of the ETM terrain.

I'm still using the ETM's lightmap generation feature for terrain self-shadowing. I may have to move away from that in the future but I'm content with it for now and have found enough things to tear my hair out about with just trying to put dynamic shadows atop the terrain w/o worrying about a movable sun.

----

I must confess, I went into this thinking to myself "Oh, this is going to be easy. I just switch it on and put some lights in right?"

HAH!


Basically, at this point I need to go do something else and let everything I just read/did sink into my brain before I can formulate an intelligent question on the subject, but I figured I'd start off a thread on the topic since I'm sure it's something others have done/are doing/are going to do.

----

Enough of the rambling preamble, the question(s) is(are):

- What is your lighting solution for your application that uses ETM?
- How's it workin' out (or not) for ya?
- Are you using custom vertex/fragment programs?
- Does anyone else think that "fragment program" sounds like something diabolical?

SongOfTheWeave

16-02-2008 07:34:06

Well, one of the more obvious problems with dynamic lighting atop ETM terrain is the issue of range.

A light affects an object if the distance between the object and the light in question is less than the range of the light.

The problem here is that the important value is, "the distance between the object and the light", not the distance between the point on the object's mesh and the light.

Since ETM tiles are relatively large and far apart you end up with one being lit by the dynamic light brightly (if its position is near the light) then the next tile not being lit at all, resulting in a nice square lit tile and various similar, undesired effects.

----

I think I need to bone up on shaders, this is starting to sound like a problem where the answer is, "write a vertex program to do that."

SongOfTheWeave

16-02-2008 08:08:33

Well, I've accomplished making all kinds of pretty lights.

Here's a shot of my tile issue. The colours are also weird... the light is sorta orange but there's no green border.



The circled things are shadows and I pointed out what object they go to in the scene. So... It's sortof working, apart from the tile issue and the fact that it seems to start lighting from the origins of whatever tiles are within it's range and gradient out from the origin regardless of the spatial relationship between the tile and the light.

er, and the colours being weird... but I wouldn't swear that I didn't just type in the colour values wrong <.<

[edit]
Oh and yes, the woman is floating off the ground, that's not a shadow bug.

Additionally, anyone know an easy way to turn off shadow casting on the SceneNode::showBoundingBox wire bounding boxes? I didn't see a way to get at those MObs.
[/edit]

CABAListic

16-02-2008 09:45:19

Well, the only kind of lights that will work flawlessly with terrain are directional lights. For the other light types there is the problem that you described - some patches are lit, some are not, depending on their origin's distance from the light. I currently see no real way to fix this; there is a MovableObject function to retrieve the light list, I'm not sure if I can do something more intelligent there, will investigate for ETM v3.

Anyway, in order to fix this, you will definitely need a shader solution for the lighting passes. And instead of the Ogre light parameters, you will probably have to pass the parameters of your point lights manually to the terrain shaders. That way, each patch will receive them regardless of its distance from the light.

SongOfTheWeave

18-02-2008 10:30:20

Does anyone have any tips or resources they could point me to on this topic?

Until yesterday I was blind deaf and dumb when it came to shaders and lately I've been spending quite a bit of time staring at the volumes of information and examples on things that seem kinda related to what I want to do (the per pixel lighting and depth shadow mapping snippets in the cookbook on the wiki most notably.)

At the moment I'm waiting for the "Ooooohhhh" moment that usually happens after I stare at stuff for long enough where it suddenly becomes clear.

----

I've ascertained thus far that this sort of thing is done in a fragment program and I'll probably use a vertex program to prime some variables per region for the fragment program to use, but exactly how to get stuff in and out of the programs is still quite foggy to me.

CABAListic

18-02-2008 12:01:07

I know how you're feeling - I pretty much stumbled into shaders for the first time when I started work on ETM. And I still only know some basics.

Anyway, what I'd recommend you to do is start with a single directional light, remove the terrain's lightmap and try to reproduce the lightmap (minus shadows) via dynamic lighting. The directional light is the simplest to compute, so it's a good starting point. Also, I'd begin with an SM2 shader pair. SM1 is a lot less comfortable to deal with and complicates things due to its restrictions, so I'd save that for later, as well.

Now, what you need is a vertex/fragment shader pair. The vertex shader comes first, it is called per vertex and gets as input some of the vertex's data. In your case you'll need the vertex position and the vertex normal. The vertex position is in local space, so first thing the shader needs to do is transform it to world space, that's why you almost always see a matrix multiplication with the world matrix. The multiplied position is then stored in the output of the shader, as well as the normal which you don't need to manipulate here.
Then the fragment shader operates on a per-pixel basis, it interpolates the output from the vertex shader to the pixel's position. Aside from the normal which you get from the vertex shader you also need the light's direction and colour which you pass in as an auto param from Ogre. The amount of light which falls on the fragment is calculated from the dot product of the terrain's normal and the light's direction, so all in all you calculate the amount of light to

colour = ambient + lightColour * abs(dot(lightDir, normal));

This is what you return as COLOR from your fragment shader. Your pass then needs to be set to scene_blend modulate.

I hope this was any helpful at all. Point lights work in some ways similar, but require slightly more work. I have no shader code handy at the moment, I guess I could write a simple example for the directional light later if you need it.

SongOfTheWeave

19-02-2008 00:19:22

Thank you! Your explanation of the shader process was very helpful.

What I've got so far is this, which is calculating the light dir rather than only supporting directional lights.

void ETDynLighting_vp
(
float4 position : POSITION,
float3 normal : NORMAL,

uniform float4 lightPosition,
uniform float3 eyePosition,

uniform float4x4 worldviewproj,

out float4 oWorldPos : POSITION,

//pass to clipping
out float4 oPos : TEXCOORD0,
out float3 oNorm : TEXCOORD1,
out float4 oLightPos : TEXCOORD2,
out float3 oEyePos : TEXCOORD3
)
{
oWorldPos = mul(worldviewproj, position);
oPos = oWorldPos;
oNorm = normalize(normal);
oLightPos = lightPosition;
oEyePos = eyePosition;
}


void ETDynLighting_fp
(
float4 pos : TEXCOORD0,
float3 normal : TEXCOORD1,
float4 lightpos : TEXCOORD2,
float3 eyepos : TEXCOORD3,

uniform float4 lightDiffuse,
uniform float4 lightSpecular,
uniform float exponent,

uniform float4 ambient,

out float4 oColor : COLOR
)
{
float3 EyeDir = normalize(eyepos - pos.xyz);
float3 LightDir = normalize(lightpos.xyz - (pos * lightpos.w));
float3 HalfAngle = normalize(LightDir + EyeDir);

float NdotL = dot(LightDir, normal);
float NdotH = dot(HalfAngle, normal);
float4 Lit = lit(NdotL, NdotH, exponent);

oColor = lightDiffuse * Lit.y + lightSpecular * Lit.z + ambient;
}


The bit I'm wondering about is whether, in the vertex program, oPos should be position or the world transformed position. And if it should be the world transformed position should oLightPos and oEyePos (camera position) be multiplied by the world transform as well?

These shaders "work" but still exhibit the terrain tile edge bug as well as LOD surface ick.

[edit]
I'm also not entirely sure what the lit function does, I just grabbed it from the per pixel lighting example which claimed it was faster... I'm looking for the API doc that describes what exactly it does.
[/edit]

[edit2]
Oh, and this is the... shader declaration I suppose it could be called

vertex_program ET/Programs/VSDynLighting cg
{
source ETDynLighting.cg
entry_point ETDynLighting_vp
profiles vs_2_0 arbvp1

default_params
{
param_named_auto lightPosition light_position_object_space 0
param_named_auto eyePosition camera_position_object_space
//param_named_auto lightPosition light_position 0
//param_named_auto eyePosition camera_position
param_named_auto worldviewproj worldviewproj_matrix
}
}

fragment_program ET/Programs/PSDynLighting cg
{
source ETDynLighting.cg
entry_point ETDynLighting_fp
profiles ps_2_0 arbfp1

default_params
{
param_named_auto lightDiffuse light_diffuse_colour 0
param_named_auto lightSpecular light_specular_colour 0
param_named exponent float 2
param_named_auto ambient ambient_light_colour
}
}


Here's the material pass:
pass Lighting
{
scene_blend modulate
iteration once_per_light

vertex_program_ref ET/Programs/VSDynLighting
{
}

fragment_program_ref ET/Programs/PSDynLighting
{
}
}


I'm not exactly sure which transform I want the various positions in... it seems to look equally wrong whether I use object space or world space. I'll post a shot in a moment.

SongOfTheWeave

19-02-2008 03:38:19



Aside from the obvious LOD issues (which I expected) the light is only being cast to the right of the lightsource. The light is a point light, and to the right in the screenshot is the positive x axis (I believe... need to put a direction reference in my toolset.

I got around the previously discussed tile object to light distance issue by setting the range of the light to 10000 (which encompasess my entire area and then some.) I suppose that might have to be my solution to the problem and pass my own "real" range to the shaders.

What I want is a soft radius cast around the point light onto the terrain that diminishes with distance until it reaches 0 intensity (or 1 intensity, I forget which one means off.)

[Edit]
I "fixed" the "only cast to the right" bug by replacing the following two lines in the FP

float NdotL = dot(LightDir, normal);
float NdotH = dot(HalfAngle, normal);


with,

float NdotL = abs(dot(LightDir, normal));
float NdotH = abs(dot(HalfAngle, normal));


I'd left that bit out from CABAL's example (interestingly it wasn't in the per pixel lighting example on the wiki.)

HOWEVER, now, instead of only having light on one side I have light on both sides with a narrow hourglass shaped gradient of unlit terrain on either side of the light where the line had been before. The light is at the narrowest point of the hourglass.

It looks as if you put two wide spotlights back to back and pointed them outwards on a post above the ground like on a siren, except that it doesn't rotate if I rotate the light.

[Edit2]
Hourglass bug:


I discovered (with much difficulty, the CG documentation I've managed to fun thus far has been pisspoor) that the y and z terms of the vector returned by the function lit(float3, float3, float) are the diffuse and specular components at that point.

SongOfTheWeave

19-02-2008 04:51:50

I tried to incorporate the LOD bit from VSLodMorph2 and ended up getting 0 lighting on the terrain at all. Here's the current state of my shaders.

Time to take a break.

void ETDynLighting_vp
(
float4 position : POSITION,
float3 normal : NORMAL,
float delta : BLENDWEIGHT,

uniform float4 lightPosition,
uniform float3 eyePosition,
uniform float4x4 worldviewproj,
uniform float morphFactor,

out float4 oWorldPos : POSITION,

//pass to clipping
out float4 oPos : TEXCOORD0,
out float3 oNorm : TEXCOORD1,
out float4 oLightPos : TEXCOORD2,
out float3 oEyePos : TEXCOORD3
)
{
position = position.y + (delta.x * morphFactor);
oWorldPos = mul(worldviewproj, position);
oPos = position;
//oNorm = normal;
oNorm = normalize(normal);
oLightPos = lightPosition;
oEyePos = eyePosition;
}


void ETDynLighting_fp
(
float4 pos : TEXCOORD0,
float3 normal : TEXCOORD1,
float4 lightpos : TEXCOORD2,
float3 eyepos : TEXCOORD3,

uniform float4 lightDiffuse,
uniform float4 lightSpecular,
uniform float exponent,

uniform float4 ambient,

out float4 oColor : COLOR
)
{
float3 EyeDir = normalize(eyepos - pos.xyz);
float3 LightDir = normalize(lightpos.xyz - (pos * lightpos.w));
float3 HalfAngle = normalize(LightDir + EyeDir);

float NdotL = abs(dot(LightDir, normal));
float NdotH = abs(dot(HalfAngle, normal));
float4 Lit = lit(NdotL, NdotH, exponent);

oColor = lightDiffuse * Lit.y + lightSpecular * Lit.z + ambient;
}


.program file
vertex_program ET/Programs/VSDynLighting cg
{
source ETDynLighting.cg
entry_point ETDynLighting_vp
profiles vs_2_0 arbvp1

default_params
{
//param_named_auto lightPosition light_position_object_space 0
//param_named_auto eyePosition camera_position_object_space
param_named_auto lightPosition light_position 0
param_named_auto eyePosition camera_position
param_named_auto worldviewproj worldviewproj_matrix
param_named_auto morphFactor custom 77
}
}

fragment_program ET/Programs/PSDynLighting cg
{
source ETDynLighting.cg
entry_point ETDynLighting_fp
profiles ps_2_0 arbfp1

default_params
{
param_named_auto lightDiffuse light_diffuse_colour 0
param_named_auto lightSpecular light_specular_colour 0
param_named exponent float 2
param_named_auto ambient ambient_light_colour
}
}

SongOfTheWeave

19-02-2008 06:54:24

Fixed the LOD issues with the following modification to the VP

void ETDynLighting_vp
(
float4 position : POSITION,
float3 normal : NORMAL,
float delta : BLENDWEIGHT,

uniform float4 lightPosition,
uniform float3 eyePosition,
uniform float4x4 worldviewproj,
uniform float morphFactor,

out float4 oWorldPos : POSITION,

out float4 oPos : TEXCOORD0,
out float3 oNorm : TEXCOORD1,
out float4 oLightPos : TEXCOORD2,
out float3 oEyePos : TEXCOORD3
)
{
oWorldPos = position;
oWorldPos.y = oWorldPos.y + (delta.x * morphFactor);
oWorldPos = mul(worldviewproj, oWorldPos);
oPos = position;
oNorm = normalize(normal);
oLightPos = lightPosition;
oEyePos = eyePosition;
}


I'd lost the .y left of the = in the morphFactor line


[Edit]
This shot makes the bug I'm still having clearly visible. I call it, the "asymptote bug"

At least I think that's how you spell asymptote. Regardless, the asymptote remains stationary regardless of where the camera moves and if I move the light it tracks it along the x axis, in other words, the hourglass artifact sweeps left to right.

CABAListic

19-02-2008 12:57:16

Hm, the latest shader code looks ok in my eyes, but I'm not much of an expert there. Anyway, what happens if you use the simpler light calculation I suggested above, i. e.
oColor = lightDiffuse * NdotL + ambient;
Granted, the distance of the light will not have any effect now, so the lighting would still be wrong. But it might help to find the actual bug.

SongOfTheWeave

19-02-2008 23:07:21

Hm, the latest shader code looks ok in my eyes, but I'm not much of an expert there. Anyway, what happens if you use the simpler light calculation I suggested above, i. e.
oColor = lightDiffuse * NdotL + ambient;
Granted, the distance of the light will not have any effect now, so the lighting would still be wrong. But it might help to find the actual bug.


Interesting... if I use the simpler light calculation I still have the asymptote beneath the light (running in the same direction) but rather than being an hourglass it's just a gradient line.

CABAListic

19-02-2008 23:20:20

Can you try normalising the normal in the fragment shader? The values are interpolated between the vertices, and I currently can't deduce if that screws their normalisation or not...

SongOfTheWeave

19-02-2008 23:53:49

Can you try normalising the normal in the fragment shader? The values are interpolated between the vertices, and I currently can't deduce if that screws their normalisation or not...

I don't see any apparent difference between normalising the normal in the vert shader vs the frag shader.

I tried to add in attenuation but it's broken. I'm working on it.

void ETDynLighting_vp
(
float4 position : POSITION,
float3 normal : NORMAL,
float delta : BLENDWEIGHT,

uniform float4 lightPosition,
uniform float3 eyePosition,
uniform float4x4 worldviewproj,
uniform float morphFactor,

out float4 oWorldPos : POSITION,

out float4 oPos : TEXCOORD0,
out float3 oNorm : TEXCOORD1,
out float4 oLightPos : TEXCOORD2,
out float3 oEyePos : TEXCOORD3
)
{
oWorldPos = position;
oWorldPos.y = oWorldPos.y + (delta.x * morphFactor);
oWorldPos = mul(worldviewproj, oWorldPos);
oPos = position;

// Moved this to frag shader
//oNorm = normalize(normal);
oNorm = normal;
oLightPos = lightPosition;
oEyePos = eyePosition;
}


void ETDynLighting_fp
(
float4 pos : TEXCOORD0,
float3 normal : TEXCOORD1,
float4 lightpos : TEXCOORD2,
float3 eyepos : TEXCOORD3,

uniform float4 lightDiffuse,
uniform float4 lightSpecular,
uniform float exponent,
uniform float4 ambient,
uniform float4 atten,

out float4 oColor : COLOR
)
{
float3 EyeDir = normalize(eyepos - pos.xyz);

// What is the pos * lightpos.w about? I got it from somewhere but removed it because it seemed weird.
//float3 LightDir = lightpos.xyz - (pos * lightpos.w);
float3 LightDir = lightpos.xyz - pos.xyz;
float dist = length(LightDir);
// Normalize this way since we already found the magnitude
LightDir = LightDir / dist;

float3 HalfAngle = normalize(LightDir + EyeDir);

normal = normalize(normal);
float NdotL = abs(dot(LightDir, normal));
float NdotH = abs(dot(HalfAngle, normal));

// THIS line produces some very interesting results. Bad, but interesting (borrowed from the monster shader on the wiki)
//oColor = ambient + (1 / (atten.y + atten.z * dist + atten.w * dist * dist)) * (lightDiffuse * NdotL + lightSpecular * NdotH);

// I made up this formula
oColor = ambient + max((1.0 - atten.y) - (atten.z * dist + atten.w * dist * dist), 0.0) * (lightDiffuse * NdotL + lightSpecular * NdotH);
}

SongOfTheWeave

20-02-2008 00:31:32

I just realized I'm doing this all in object space... Should I be doing it in camera space or world space?

CABAListic

20-02-2008 00:35:22

The maths should be independent of the coordinate system. You just have to make sure that ALL your positions are given in the same system. Especially with the Ogre provided positions.

SongOfTheWeave

20-02-2008 08:15:59

Soooo, you know how the stupidest mistakes are always the hardest to find?

I had been accepting the default argument for createNormals in TerrainManager::createTerrain... which is false. So... I guess 0,1,0 was what the shader was getting for ALL normals. So, I fixed that, and the lighting works a lot more like I thought it would given the shader code.

After feeling like an idiot for a while, I started back working on the attenuation stuff. I've reached the conclusion that "dist" must be incorrect in the fragment shader. The light attenuation is done correctly on other objects, but on the terrain it is lit 100% all the way to the limit of the range (at which point you get a "tile stairstep" edge, so I try to make range far enough that you dont' see the edge of it.)

Screenshot:


In this shot, diffuse is red (1 0 0 1) and specular is blue (0 0 1 1)

The red parts (only diffuse) wiggle around as you move the camera in a way that seems fairly natural (well, as natural as red splotches on purple terrain could possibly look) and also respond to movements of the light even when the camera is stationary, so it seems to be working somewhat properly. The problem is that specular light seems to be either on or off. In fact, all the lighting seems to be either on or off. What I want to see are smooth gradients.

------

My current theory, as I mentioned above, is that dist is not being calculated correctly in the frag shader. A few questions to explore this theory (full shader code below):

- From the vertex shader:
float4 position : POSITION,
Is this position in object space? I assume it is because you need to multiply it by the world tranform or am I backwards and it begins in world space and you transform it into object space?

- Also in the vertex shader:
CABAL, do I want to send the frag program the LOD shifted position or the unshifted position? I'm guessing shifted but I'm not really sure.

- General:
o - Why does POSITION come in as a float4? Whats in the 4th float?
o - [stupidquestion]w is the 4th component of a 4 component vector right? ex. position.w , in other words:
float4 identifier = (x, y, z, w)
Right? I haven't been able to find decent CG documentation yet.
[/stupidquestion]

------

Current Shaders:

void ETDynLighting_vp
(
float4 position : POSITION,
float3 normal : NORMAL,
float delta : BLENDWEIGHT,

uniform float4 lightPosition,
uniform float3 eyePosition,
uniform float4x4 worldviewproj,
uniform float morphFactor,

out float4 oWorldPos : POSITION,

out float4 oPos : TEXCOORD0,
out float3 oNorm : TEXCOORD1,
out float3 oLightPos : TEXCOORD2,
out float3 oEyePos : TEXCOORD3
)
{
position.y = position.y + (delta.x * morphFactor);
oPos = position;
oWorldPos = mul(worldviewproj, position);
//oNorm = normalize(normal);
oNorm = normal;
oLightPos = lightPosition;
oEyePos = eyePosition;
}


void ETDynLighting_fp
(
float4 pos : TEXCOORD0,
float3 normal : TEXCOORD1,
float4 lightpos : TEXCOORD2,
float3 eyepos : TEXCOORD3,

uniform float4 lightDiffuse,
uniform float4 lightSpecular,
uniform float exponent,
uniform float4 ambient,
uniform float4 atten,

out float4 oColor : COLOR
)
{
float3 EyeDir = normalize(eyepos - pos.xyz);

float3 LightDir = lightpos - pos;
float dist = length(LightDir);
// Normalize this way since we already found the magnitude
LightDir = LightDir / dist;

float3 HalfAngle = normalize(LightDir + EyeDir);

normal = normalize(normal);
float NdotL = abs(dot(LightDir, normal));
float NdotH = abs(dot(HalfAngle, normal));
float4 Lit = lit(NdotL, NdotH, exponent);

float fLuminence = 1 / (atten.y + atten.z * dist.x + atten.w * dist.x * dist.x);
oColor = ambient + (fLuminence * (lightSpecular * Lit.z + lightDiffuse * Lit.y));
}

CABAListic

20-02-2008 10:38:21

Well, I'm glad you got it somewhat working :)
I have no immediate answer to your remaining problem, but I'll try to answer your questions as good as I can.


- From the vertex shader:
float4 position : POSITION,
Is this position in object space? I assume it is because you need to multiply it by the world tranform or am I backwards and it begins in world space and you transform it into object space?

No, no, it's in object space, and you need to output the final position in world space. Most of the time, that's just a multiplication with the world transform matrix, but in case of terrain you would also want to move the vertex by its delta values to get smooth LOD transitions.


- Also in the vertex shader:
CABAL, do I want to send the frag program the LOD shifted position or the unshifted position? I'm guessing shifted but I'm not really sure.

The shifted position. Though it might be worth a try to completely remove the LOD morphing for the time being, just to rule out another potential source of error.

- General:
o - Why does POSITION come in as a float4? Whats in the 4th float?
o - [stupidquestion]w is the 4th component of a 4 component vector right? ex. position.w , in other words:
float4 identifier = (x, y, z, w)
Right? I haven't been able to find decent CG documentation yet.
[/stupidquestion]

(x, y, z, w) is correct, yes. In case of positions, the w component will always be 1, this is a mathematical trick.
In order to get from local space to world space, you essentially need to take 3 things into account: the translation, the rotation and the scale of the object in world space. Translation can be represented by a Vector3 in 3D space, the rotation and scale via a 3x3 matrix or a quaternion, for example. So, if R is the rotation matrix and T the translation vector, the world position would be calculated by worldPos = R*localPos + T.
Now, instead of that both translational and rotational/scaling parts are put together in a single 4x4 matrix. This matrix then looks as follows:

R11 R12 R13 T1
R21 R22 R23 T2
R31 R32 R33 T3
0 0 0 1

And localPos is extended to a Vector4, with the fourth component set to 1. You can do the matrix multiplication, you'll see that it has the same effect as the formula above.

SongOfTheWeave

20-02-2008 11:13:14

Thanks for your reply CABAL, helpful as always.

On a similar note, I figured out why light position is a float4 too. Heh, it was in the manual :? And in figuring that out, I figured out what the below line is all about.

float3 LightDir = lightpos.xyz - (pos * lightpos.w);

This strange line allows directional lights and point lights to be handled without any branching (i.e. if statements) which I've heard are bad in shaders, presumably for performance reasons.

The way ogre handles the light position shader input parameters is, if the light is a point light it puts (posX, posY, posZ, 1.0) into the float4. If the light is a directional light it puts (dirX, dirY, dirZ, 0.0) into the float4.

So, in the above line, if the light is a point light, pos is multiplied by 1.0 and the proper vector subtraction is performed. If the light is a directional light, pos is multiplied by 0.0 and the subtraction has no effect on lightpos which (since this is a directional light) contains the direction of the light. So you end up with the right LightDir either way.

I love clever things like that.

SongOfTheWeave

20-02-2008 11:42:22

It's beautiful... *sniffles and wipes a tear*



I'll post shaders and explain what I did tomorrow. As for now, I'm going to a blissful sleep free of nightmares of ravenous fragment programs.