PDA

View Full Version : [HLSL] Diffuse lighting oddities...


kaotrix
03-18-2009, 05:59 PM
Hello,

I've spent the last couple of days trying to get into HLSL and to eventually use fancy shaders in my future games. Currently, I'm following Frank D. Luna's excellent book entitled, "Introduction to 3D Game Programming w/ DirectX 9: A Shader Approach". So far, I'm at chapter 10--this chapter discusses various lighting techniques including: ambient, diffuse, specular, point, spotlight, phong shading, etc.

I've ran into an unusual problem with my 3D application, unfortunately. The solution is probably simple and perhaps I'm overlooking something small, or perhaps my math isn't what it's supposed to be. In my 3D application I have four rows of spheres laid out in a grid. This grid of spheres are all located int the positive xz plane. In my shader I set up a directional diffuse light that points along the positive z-axis (0, 0, 1}. Additionally, my movable camera is set up in the position {0, 0, -10} with the view target at {0, 0, 0} (looking towards the grid of spheres on the xz plane).

With the aforementioned details, the expected result is for all of the spheres in the grid to be lit on the {0, 0, -1} side. However, the result is quite opposite. The side of the spheres that aren't supposed to be lit are lit, and vice-versa for the opposite side.

Here is my diffuse lighting shader in its entirety:

struct InputVS
{

float4 position : POSITION ;
float4 normal : NORMAL ;
} ;

struct OutputVS
{

float4 position : POSITION ;
float4 diffuse : COLOR ;
} ;

uniform extern float4x4 mtrxWVP : WorldViewProjection ;
uniform extern float4x4 mtrxWorld_IT : WorldInverseTranspose ;

// General light attributes
const float4 vLightDir = { 0.0f, 0.0f, 1.0f, 0.0f } ;

// Ambient lighting attributes
uniform extern float4 ligAmbient ;
uniform extern float4 mtrlAmbient ;

// Diffuse lighting attributes
uniform extern float4 ligDiffuse ;
uniform extern float4 mtrlDiffuse ;

OutputVS MainVS( InputVS input )
{

OutputVS output = (OutputVS)0 ;
float3 ambient, diffuse ;
float diffuse_co = 0.0f ;

input.position.w = 1.0f ;
input.normal.w = 0.0f ;

output.position = mul( input.position, mtrxWVP ) ;

input.normal = mul( input.normal, mtrxWorld_IT ) ;
input.normal = normalize( input.normal ) ;

diffuse_co = max( dot( vLightDir, input.normal ), 0 ) ;

ambient = (ligAmbient * mtrlAmbient).rgb ;
diffuse = diffuse_co * (ligDiffuse * mtrlDiffuse).rgb ;

output.diffuse.rgb = ambient + diffuse ;
output.diffuse.a = 1.0f ;

return output ;
}

float4 MainPS( float4 diffuse : COLOR0 ) : COLOR
{

return diffuse ;
}

technique MainFX
{

pass p0
{

AlphaBlendEnable = TRUE ;
Ambient = float4( 1.0f, 1.0f, 1.0f, 1.0f ) ;
CullMode = CCW ;
DestBlend = INVSRCALPHA ;
FillMode = SOLID ;
Lighting = FALSE ;
NormalizeNormals = FALSE ;
ShadeMode = GOURAUD ;
SpecularEnable = FALSE ;
SrcBlend = SRCALPHA ;
StencilEnable = FALSE ;
ZEnable = TRUE ;
ZFunc = LESSEQUAL ;

VertexShader = compile vs_2_0 MainVS() ;
PixelShader = compile ps_2_0 MainPS() ;
}
}



Here is the c++ code snippet detailing how the spheres are rendered:

void CDemo::DrawSpheres( float fCount, float fSpacing )
{

float fRoot = sqrt( fCount ) ;

if( !isWholeNumber( fRoot ) )
return ;

D3DXVECTOR4 mtrlAmbient( 0.5f, 0.0f, 0.0f, 1.0f ) ;
_pEffect->SetVector( _hmtrlAmbient, &mtrlAmbient ) ;

D3DXVECTOR4 mtrlDiffuse( 0.9f, 0.0f, 0.0f, 1.0f ) ;
_pEffect->SetVector( _hmtrlDiffuse, &mtrlDiffuse ) ;

D3DXMATRIX mtrxWorldViewProj, mtrxWorld_IT ;
D3DXMATRIX mtrxWorld, mtrxView, mtrxProj ;

_pGDevice->GetTransform( D3DTS_VIEW, &mtrxView ) ;
_pGDevice->GetTransform( D3DTS_PROJECTION, &mtrxProj ) ;

for( unsigned int j = 0 ; j < (unsigned int)fRoot ; j++ )
{

for( unsigned int k = 0 ; k < (unsigned int)fRoot ; k++ )
{

D3DXMatrixTranslation( &mtrxWorld, k * (5.0f + fSpacing), 2.5f, j * (5.0f + fSpacing) ) ;

mtrxWorldViewProj = mtrxWorld * mtrxView * mtrxProj ;
_pEffect->SetMatrix( _hmtrxWVP, &mtrxWorldViewProj ) ;

D3DXMatrixInverse( &mtrxWorld_IT, 0, &mtrxWorld ) ;
D3DXMatrixTranspose( &mtrxWorld_IT, &mtrxWorld_IT ) ;
_pEffect->SetMatrix( _hmtrxWorld_IT, &mtrxWorld_IT ) ;

_pEffect->CommitChanges() ;
_mshBall->DrawSubset( 0 ) ;
}
}
}

//************************************************** **********




Now for the screenshots of my 3D application. The first screen shows the grid of spheres rendered when the shader variable, 'vLightDir', is set to {0, 0, 1}. Please note that the front of spheres are supposed to be lit, not the back (which is my problem):

http://celestialtree.org/demo/Demo_01.jpg

This 2nd screenshot shows the application at work while the shader variable, 'vLightDir', is set to {0, 0, -1}. Note how the spheres are lit, very unusual. Also note how the first sphere in the grid (lower left corner) is perfectly lit; very bizarre.

http://celestialtree.org/demo/Demo_02.jpg

Goz
03-19-2009, 07:32 AM
Erm .. im slightly confused by your world inverse transpose. Surely this is taking you back to a local lighting space. I would have thought you'd be better transforming your normals into view space (ie simply multiply by world).

Try passing in mtrxWorld instead of mtrxWorld and see what happens.

The first image is, completely correct. You are looking at the unlit side of the spheres and they AREN'T lit at all. They are just taking your ambient value of 0.5 in red (Which is a little on the high side, imo). It usual to use -vLightDir in your dot product calculation btw. As other wise you get the highest results when the normal and light direction are pointing the same direction. You want the opposite of that (ie 1 when they are pointing exactly opposite each other).

kruseborn
03-19-2009, 08:44 AM
I would say its better to give the light as a position in world space not as a vector, examples (0,0,-1), then you want the vector from the vertex to the light.

float4 worldPos = mul(float4(input.pos, 1.0f), mWorld);
lightVector = normalize(lightPos - worldPos)

Now you have your light vector in world space and now you want your normal in world space. Something like this

output.norm = normalize(mul(float4(input.norm, 0.0f), mWorld));

Now you can do the dot product.

starstutter
03-19-2009, 08:47 AM
yes, I belive the mtrxWorld_IT is your problem. The matrix should not be inversed. Did it say that in the book?

starstutter
03-19-2009, 08:49 AM
I would say its better to give the light as a position in world space not as a vector, examples (0,0,-1), then you want the vector from the vertex to the light.

Well I'm not real sure about that. I think as long as your doing directional lighting, a vector should do fine.

kruseborn
03-19-2009, 09:07 AM
Well I'm not real sure about that. I think as long as your doing directional lighting, a vector should do fine.

Yes its true, I was thinking point lights

Reedbeta
03-19-2009, 09:59 AM
The matrix calculation looks correct to me on a once-over. Goz and starstutter, you guys do know you have to use the inverse transpose for normals, correct? It's the same as the world matrix unless you have scaling (in particular, nonuniform scaling is where it's really necessary). If you think about it, when you nonuniformly scale geometry, you don't want to scale the normals in the same way - they won't stay perpendicular to their faces.

Goz
03-19-2009, 12:47 PM
The matrix calculation looks correct to me on a once-over. Goz and starstutter, you guys do know you have to use the inverse transpose for normals, correct? It's the same as the world matrix unless you have scaling (in particular, nonuniform scaling is where it's really necessary). If you think about it, when you nonuniformly scale geometry, you don't want to scale the normals in the same way - they won't stay perpendicular to their faces.

yeah .. you have a fair point there.

Then the only thing I can imagine being wrong is that the normals are wrong then.

Can you output the normal as a colour?

ie


output.diffuse = float4( (input.normal.xyz * 2.0) - 1.0, 1.0 );


Post up an image of your normals and we'll make sure they look right. Probably worth posting up an image of the normals pre and post the inverse-transpose transform ...

Goz
03-19-2009, 12:49 PM
also ... isn't it transpose then inverse? Or am i going mad?

Reedbeta
03-19-2009, 05:12 PM
Doesn't matter; inverse(transpose(M)) == transpose(inverse(M)).

kaotrix
03-21-2009, 04:53 AM
Don't worry guys I've already solved the issue. The light direction is a vector going from the vertex's 3D position to the light source. As for the other oddity, the incoming normal data in the vertex shader should be float3 not float4.

I have some additional questions however as I proceed further into this book. And I'd prefer a non-scientific response to these questions because I've googled like crazy and can't wrap my head around some of these terminologies.

Phong Shading
From what I've read Phong Shading is a vertex shading technique which involves interpolating vertex normals during rasterization. When you send the vertex data from the vertex shader to the pixel shader, sometimes, an interpolated normal can become un-normalized.

1) In Flash animation I've learned that interpolating means to approximate data between two key frames. Is the process of interpolating vertex normals computationally expensive?

2) Sometimes non-interpolated normals aren't normalized. Normalizing interpolated normals in the pixel shader doesn't seem like a good idea since the square-root (a slow function from what I can gather) function is required when normalizing vectors. Is there a better way in ensuring that all interpolated normals are already normalized after the vertex shader is done with the data?

3) In using phong shading my geometry appears smoother. From that I conclude it to be a superior shading method to my old ambient-diffuse-specular lighting shader. Is the phong shading technique an industry standard/norm? Or is there a superior technique I'll learn about later on?

Transformations

1) What exactly is homogeneous space? From what I can tell all of the points in a homogeneous space have a 4th component, w, which is equal to 1 on all points (w = 1). What is the significance of this fourth component? Why is it needed? And how does a homogeneous space differ from a normal 'ole 3D space (xyz).

2) Visually, what is the distinction between transforming a point to world/model space and transforming it to World-View-Projection?

Some Shader Code
1) And finally (sorry for the questions, this is my last). This might be a dumb question but I haven't really learned much about pixel shaders up until this point. In the following code segment:


struct OutputVS
{
float4 posH : POSITION0;
float3 normalW : TEXCOORD0;
float3 posW : TEXCOORD1;
};

OutputVS PhongVS(float3 posL : POSITION0, float3 normalL : NORMAL0)
{
// Zero out our output.
OutputVS outVS = (OutputVS)0;

// Transform normal to world space.
outVS.normalW = mul(float4(normalL, 0.0f),
gWorldInverseTranspose).xyz;
outVS.normalW = normalize(outVS.normalW);

// Transform vertex position to world space.
outVS.posW = mul(float4(posL, 1.0f), gWorld).xyz;

// Transform to homogeneous clip space.
outVS.posH = mul(float4(posL, 1.0f), gWVP);

// Done--return the output.
return outVS;
}

float4 PhongPS(float3 normalW : TEXCOORD0,
float3 posW : TEXCOORD1) : COLOR
{
// Compute the color: Equation 10.3.

// Interpolated normals can become unnormal--so normalize.
normalW = normalize(normalW);

// Compute the vector from the vertex to the eye position.
float3 toEye = normalize(gEyePosW - posW);

// Compute the reflection vector.
float3 r = reflect(-gLightVecW, normalW);

// Determine how much (if any) specular light makes it
// into the eye.
float t = pow(max(dot(r, toEye), 0.0f), gSpecularPower);

// Determine the diffuse light intensity that strikes the vertex.
float s = max(dot(gLightVecW, normalW), 0.0f);

// Compute the ambient, diffuse, and specular terms separately.
float3 spec = t*(gSpecularMtrl*gSpecularLight).rgb;
float3 diffuse = s*(gDiffuseMtrl*gDiffuseLight).rgb;
float3 ambient = gAmbientMtrl*gAmbientLight;

// Sum all the terms together and copy over the diffuse alpha.
return float4(ambient + diffuse + spec, gDiffuseMtrl.a);
}


In the struct OutputVS, why are 'normalW' and 'posW' labeled with TEXCOORD0 and TEXCOORD1 respectively? From what I can gather, these are called registers and they tell the GPU how the variables are to be used. But I don't understand why these two variables are passed onto the pixel shader as texture coordinates. Texture coordinates usually have {u, v} points but these variables are defined with float3 (x, y, z).

starstutter
03-21-2009, 08:51 AM
In the struct OutputVS, why are 'normalW' and 'posW' labeled with TEXCOORD0 and TEXCOORD1 respectively? From what I can gather, these are called registers and they tell the GPU how the variables are to be used. But I don't understand why these two variables are passed onto the pixel shader as texture coordinates. Texture coordinates usually have {u, v} points but these variables are defined with float3 (x, y, z).

To tell you the truth I don't totally understand either in that reguard. What I can say is that when information (verticies) are coming into the VS, those declarations tell the GPU what infromation to use. If you had an FVF like this:

FVF = (XYZ | NORMAL | TEXCOORD0);

and data structure like this:

struct {
d3dxvector3 position
d3dxvector3 normal
d3dxvector2 uv }

then using the TEXCOORD0 will make the VS draw information from the third variable (uv) in the data structure that makes up each vertex. That's the basic idea anyway. As for transfering data from the VS to the PS, I know that the POSITION declaration makes a big difference in how the passed data is handled, but I don't think it makes a difference for other types.

If you are using a custom VS, but not a PS (still using the fixed function pipeline) then they probably would make a difference. Beyond that I'm not real sure. 'normalW' and 'posW' are most likley labeled with the TEXCOORD declarations because there's just no technical reason to be specific with their types. Perhaps its bad practice, but it wouldn't cause any problems in the code.

Hope this helps.

Reedbeta
03-21-2009, 10:46 AM
1) In Flash animation I've learned that interpolating means to approximate data between two key frames. Is the process of interpolating vertex normals computationally expensive?


Not especially. Graphics hardware can interpolate all manner of things for very little cost.


2) Sometimes non-interpolated normals aren't normalized. Normalizing interpolated normals in the pixel shader doesn't seem like a good idea since the square-root (a slow function from what I can gather) function is required when normalizing vectors. Is there a better way in ensuring that all interpolated normals are already normalized after the vertex shader is done with the data?

Actually, normalizing in the pixel shader is not that bad on modern GPUs. It is a relatively expensive operation, but many GPUs have dedicated normalize units built right in, which are quite efficient. Normalizing in the pixel shader is the standard way to deal with this problem.


3) In using phong shading my geometry appears smoother. From that I conclude it to be a superior shading method to my old ambient-diffuse-specular lighting shader. Is the phong shading technique an industry standard/norm? Or is there a superior technique I'll learn about later on?


Phong shading is a very standard technique in the industry, although there's a slight variation called Blinn shading which seems to be somewhat more popular. Most of the more-advanced shaders in video games are basically Phong/Blinn at heart, but "pimped out" with additional features, like normal mapping, ambient occlusion, or parallax mapping.


1) What exactly is homogeneous space? From what I can tell all of the points in a homogeneous space have a 4th component, w, which is equal to 1 on all points (w = 1). What is the significance of this fourth component? Why is it needed? And how does a homogeneous space differ from a normal 'ole 3D space (xyz).

The sole purpose of this is to make it convenient for us to express (a) translations and (b) perspective-projections in a matrix form.

I'll assume you've encountered matrices already. They are a convenient form for representing transformations because they can very easily be multiplied together to compose transformations. However, matrices can fundamentally only represent *linear* transformations. In 3D space, linear transformations include scaling, rotation, and reflection, but not translation or perspective-projection. Appending the 4th component allows us to "embed" plain old 3D space into a larger, 4D space called homogeneous space. This is useful because translation and perspective-projection become linear in this space and therefore can be represented by matrices. So now we have all the transformations we want together in one big happy family and we can compose them at will.

(As an aside: if translations were the only problem, we could get those by making a straightforward generalization of matrices to what are called affine transformations, and we wouldn't need a 4th component on each vector for this. But perspective-projection absolutely requires the full 4D space.)


2) Visually, what is the distinction between transforming a point to world/model space and transforming it to World-View-Projection?


Model, world, and view spaces are all just plain 3D spaces, so the w-component will always stay equal to 1 in these. Really they just correspond to moving around and reorienting the coordinate axes.

When you transform by the projection matrix, though, some more complicated things happen. The projection matrix is what actually gives you perspective, i.e. things look smaller when they're far away. This is what turns the 3D space into something that you can make sense of as a 2D image.

I'm not sure I actually answered your question here; feel free to clarify if needed.

In the struct OutputVS, why are 'normalW' and 'posW' labeled with TEXCOORD0 and TEXCOORD1 respectively? From what I can gather, these are called registers and they tell the GPU how the variables are to be used. But I don't understand why these two variables are passed onto the pixel shader as texture coordinates. Texture coordinates usually have {u, v} points but these variables are defined with float3 (x, y, z).

Texture coordinates actually support a full 4D vector. It's true that usually you only use the first two components, for identifying a point in a 2D texture. However there are also 3D textures, as well as cube map textures, and these use a 3rd coordinate. Finally, there are projective textures, which allow you to do a perspective-projection of the texture coordinates (this is handy for cookie lights, shadow mapping, and a few other things), and this uses all 4 coordinates.

Anyway, it's standard practice in real-time graphics to use the texture coordinate registers as general purpose registers for passing whatever data your shader happens to need. It's because historically, we didn't have vertex and pixel shaders, only fixed-function pipelines that you had to configure to do what you want by setting various params and flags. In those days each register had a fixed purpose such as color, normal, texture coordinate for texture unit 0, etc. When shaders started coming out, they existed in parallel with the fixed-function stuff for awhile, and so people writing shaders sort of piggybacked on the registers that were already there.

Nowadays, though, we're moving in the direction of just having a set of generic registers that aren't tied to any particular use.