PerPixel lighting looking like PerVertex lighting

Started by
25 comments, last by RobM 15 years, 6 months ago
Hi all I'm using DirectX9 and ps3/vs3. I'm having a bit of an issue with my normals. My terrain's patches are made of of quads like this:
*---*---*
|  /|  /|
| / | / |
|/  |/  |
*---*---*
I have, for now, uncompressed normals at each vertex and when rendered, it appears like the terrain is lighting per vertex rather than per-pixel. Here's a solid screenshot (link) and a wireframe one (link). It's mostly noticeable when the terrain slopes across the diagonal, or in other words, when the diagonal of the quads lie adjacent to the direction of the terrain 'hip' or bump. Do I need to switch anything on to use per-pixel lighting? I thought you just got shading per-pixel by using pixel shaders. Here's a snippet of my very simple pixel shader:
...
float4 m = float4(0.288f, 0.408f, 0.631f, 1.0f);
float s = saturate(dot(lightVec, In.Normal));
m = m+s; // this is m plus s (don't know why the plus sign isn't showing)
return m;
}
All it is doing is using the light to change from light blue to white. Any ideas? Thanks all
Advertisement
Quote:Original post by RobMaddisonI thought you just got shading per-pixel by using pixel shaders.


No, it doesn't work this way, because the quality of the normal is what's ultimately important. Here you are just taking the per-vertex normal from the pixel shader and doingto dot product in the pixel shader.

What you need is a per-pixel normal, usually this involves a normal map which gives a much more detailed normal very cheaply, but you don't have to use one.

Here is what you generally do:

1) Make sure your mesh vertices have a precalculated tangent vector.
2) in the pixel shader create a tangent-binormal-normal matrix:

half3x3 worldToTangentSpace;
worldToTangentSpace[0] = mul(in.tangent, matWorld);
worldToTangentSpace[1] = mul(cross(in.normal,in.tangent), matWorld);
worldToTangentSpace[2] = mul(in.normal, matWorld);

3) transform your light and output
Out.light.xyz = mul(worldToTangentSpace, vecSunDir);

4) In the PIXEL SHADER normalize this light vector...the normalization is important:

float3 Sun=normalize(In.light);

5) then do your dot product...you can use a normal map like this:

float3 normalmap=tex2D(normalsamp,intex.xy)*2-1;
float diffuse=saturate(dot(normalmap,Sun));

If you dont want a normal map just use a flat normal like

float3 normalmap=float3(0.5,0.5,1)*2-1;

---------------------------------------------------------------------------

Note: for a much simpler method without a normal map, I THINK you can do this; dont bother with the tangent stuff; just renormalize the normal in the pixel shader and then do the dot product... you should get reasonable per-pixel lighting.
Thanks Matt

I was under the impression that the normal passed from the Vertex shader to the pixel shader was already interpolated. So you're saying you have to manually do this?

I don't have any normal maps (yet), I simply have a [pre-calculated] averaged normal per vertex. This comes into the vertex shader via a stream.

I thought the whole point of vertex/pixel shaders was that whatever you passed from the vertex to the pixel shader was interpolated ready for the pixel shader to use. I guess not :)

I'll look into your suggestion, thanks.

Rob
Quote:Original post by RobMaddison
Thanks Matt

I was under the impression that the normal passed from the Vertex shader to the pixel shader was already interpolated. So you're saying you have to manually do this?


No no it's interpolated. What you're doing in your simple pixel-shader is definitely per-pixel lighting. It looks like your problem there is that something is a little wacky with your normals, but I'm not sure what. Hopefully a terrain expert will see this and can help you out further, it's not something I'm particularly knowledgeable about. [smile]

EDIT: make sure you normalize your normal in the pixel shader, you don't get a unit vector when you interpolate between two unit vectors.
Vertex lighting means to calculate the reflection on each vertex and to let the interpolator calculate the in-between values. If, on the other hand, the vertex normals and light vectors are interpolated and the reflectance is computed by the pixel shader, then per-pixel lighting is done. That is because normal interpolation _does_ give you a normal per pixel (so to say). Sure, a normal map allows to have more surface details, but it is in no way a _requirement_ for per-pixel lighting. Normal maps are just a way to enhance the mesh's surface structure without raising the spatial resolution of the mesh.

Assume a small specular reflectance lobe that resides between 2 vertices. Using vertex lighting, the slobe is sampled in the offshoots by the 2 vertices, and hence the interpolated reflactance is everywhere, well, flat. But if the vectors are interpolated and the product is computed in the pixel shader, then the sampling is done (typically) with a higher resolution, the lobe isn't missed, and a nice highlight will be rendered. q.e.d. :)

However, how looks the vertex script like? What's with the normalization Matt has mentioned? Can you show us all belonging parts of the scripts?


Just for nit-picking: A "bi-normal" is part of the co-ordinate frame (e.g. Frenet frame) on a curve. When dealing with a surface, we have a "bi-tangent" instead. Don't know when the term bi-normal has happened to surfaces, perhaps in the semantic declarations for vertex buffers? Never mind. It is used often synonymously, although not being really correct.
It does look like your actual per-vertex normals are off - have you tried drawing them (as lines originating from each vertex is usually quite easy) to see if they look correct?
This looks suspicious:

float s = saturate(dot(lightVec, In.Normal));

The 2 vectors you dot must be normalized per-pixel. That is, it should be dot(normalize(lightVec),normalize(In.Normal)).

Also, *very important*: The light vector should *not* be normalized in the vertex shader. It should be interpolated unnormalized, and normalized only in the pixel shader. Normalizing it in the vertex shader(prior to interpolation) will definately yield incorrect results.

You could post your full vertex and pixel shaders for more details.
the point i was trying to make is that with diffuse lighting in a situation like this, plain per pixel-lighting (certainly your normal is interpolated) is not always a big win in terms of quality, that's why I suggested normal maps...it's a low cost way to correct almost any problem due to to low vertex density (other than mesh fixes).

Other than that, the result in your screen shot doesn't look like anything is wrong, its just the vertex density is too low and too discontinuous to produce very smooth lighting without higher frequency normal data. Per-pixel interpolation itself isnt a magic bullet with meshes like this (although for specular lighting is a huge improvement..try doing a specular term in your code and you will see...it it looks bad then you know something is wrong.)
Hi guys

Thanks again for all your valuable input, this site is brilliant.

Here's my cut-down shader (i've removed my texturing stuff for clarity which wasn't being used for this issue anyway):

struct V_To_P{    float4 Position	: POSITION;    float3 Normal	: NORMAL;};float1 startPositionX;float1 startPositionY;float4x4 xViewProjection;float4x4 xWorld;float3 lightVec;V_To_P SimpleVertexShader( float3 Position : POSITION, float4 YPos : TEXCOORD, float3 Normal : NORMAL){	V_To_P Output = (V_To_P)0;		float4 newPos = float4(Position.x, YPos.x, Position.z, 1.0);	float4 xFormedPos = mul(newPos, xWorld);	Output.Position = mul(xFormedPos, xViewProjection);			Output.Normal = Normal;	return Output;}float4 SimplePixelShader(V_To_P In ) : COLOR0{	float4 m = float4(0.288f, 0.408f, 0.631f, 1.0f);	float s = saturate(dot(normalize(lightVec), normalize(In.Normal)));	return m + s;}technique Simplest{	pass Pass0	{		VertexShader = compile vs_3_0 SimpleVertexShader();		PixelShader = compile ps_3_0 SimplePixelShader();	}}


The Position parameter contains the x and z of the terrain, the YPos parameter contains the height, and Normal contains the normal.

I'm fairly confident that the normals are correct but I will double-check them. My lightVec constant var is normalized when initially sent into the vertex shader (by setting the lightVec constant var).

Is everything interpolated between the vertex shader and pixel shader? I mean in my V_To_P type, I've specified Normal, but I haven't explicitly told the interpolator to interpolate it. Does it work that out from the semantic?

Thanks
Rob
The results look correct to me. You'll notice a huge difference if you try to place a point light close to the terrain; but in this case, your terrain is lit by a directionnal light, and it doesn't have any specular reflection. Per-pixel lighting won't improve the results much over per-vertex lighting.

Y.

This topic is closed to new replies.

Advertisement