Parentheses in HLSL cause light attenuation function to not work correctly?

Started by
13 comments, last by Bacterius 11 years, 7 months ago
I have a diffuse+specular equation in my pixel shader, and it works pretty well except for this one issue:

When I change this: float attenuation = 1.0f / d*d;
To this: float attenuation = 1.0f / ( d*d );

My model is no longer lit, and is instead the color of my ambient intensity. I find this extremely strange. The reason I want parentheses is so I can use a different attenuation function such as ( 1 + 0.045*d + 0.0075*d*d ).

It also messes up if I try this:

float denominator = d*d;
float attenuation = 1.0f / denominator;



Here is my entire pixel shader. The reason for all of the "tmp_stuff" variables is to make everything "politically correct" since I have no choice but to input them as float4 due to constant buffer alignment properties. I would rather convert everything to temporary float3 rather than risk the chance of something not working right because of float4. Anyways, it shouldn't have anything to do with my current problem. I've doubled, tripled, and quad-checked my lighting equations with four different books, and I'm just baffled with this problem.


void ps( in v2p input, out float4 final_color : SV_TARGET )
{
float3 ambient_intensity = float3( 0.1f, 0.1f, 0.1f );
float3 diffuse_color = float3( 0.8f, 0.8f, 0.8f);
float3 specular_color = float3( 1.0f, 1.0f , 1.0f );

float3 tmp_light;
tmp_light.x = light_vector.x;
tmp_light.y = light_vector.y;
tmp_light.z = light_vector.z;

float3 norm_light = normalize( tmp_light );

float3 tmp_pos;
tmp_pos.x = input.pos.x;
tmp_pos.y = input.pos.y;
tmp_pos.z = input.pos.z;

float3 tmp_norm;
tmp_norm.x = input.norm.x;
tmp_norm.y = input.norm.y;
tmp_norm.z = input.norm.z;

float3 tmp_cam = float3( 0.0f, 0.0f, -20.0f ); // fixed view camera position

// light intensity
float d = distance( tmp_pos, tmp_light );
float attenuation = 1.0f/d*d; // HERE IS THE PROBLEM AREA

float3 pointlight = attenuation*float3( light_color.x, light_color.y, light_color.z );

// diffuse lighting
float diffuse = max( dot( tmp_norm, norm_light) , 0.0f );
float3 diffuse_final = diffuse_color*ambient_intensity + diffuse_color*pointlight*diffuse;

// specular lighting
float3 reflect_vect = 2*dot( tmp_norm, norm_light )*tmp_norm - norm_light;
float ref_max = max( dot( reflect_vect, normalize(tmp_cam) ), 0.0f );
float spec_exponent = pow ( ref_max, 50.0f );
float3 spec_final;

if( dot( tmp_norm, norm_light ) <= 0 )
{
spec_final = float3( 0.0f, 0.0f, 0.0f );
}
if( dot( tmp_norm, norm_light ) > 0 )
{
spec_final = specular_color*pointlight*spec_exponent;
}
final_color = float4( diffuse_final + spec_final, 1.0f );
}


Without parentheses:
357rmnq.png

With parentheses:
70jscy.png
Advertisement
That's because in your first statement you didn't appropriately handle the order of operations. If you made any tweaks as to how you calculated your attenuation values to work with the old setup, it's most likely incorrect.
Perception is when one imagination clashes with another
Hi!

If you write
float attenuation = 1.0f / d*d;
if will evaluate to [formula] attenuation = 1.0 \frac{d}{d}=1.0[/formula]. Therefore, you haven’t had any attenuation at all.

If you change it to
float attenuation = 1.0f / (d*d);
you got what you want: [formula] attenuation = 1.0 \frac{1}{d^2}[/formula]

By the way, the other attenuation parameters (constant and linear) are just for artistic purposes. Squared attenuation would be most correct, since the irradiance of a point light is:
E = max(0, cosAngle) * vLightIntensity / (squaredDistance);
For a diffuse surface the radiance then becomes:
L = E * vSurfaceColor / Pi;
You got it right, except for the division by Pi. (It is there for the energy conservation.)

Anyway, the reason why things are getting dark is, that your light source is probably too far away. Consider a distance of 10 units in space. Squared and divided gives you 1/100 of your un-attenuated intensity. I think, things should work for you, if you make your light brighter, i.e. your variable “lightColor”. The lightColor actually stores the luminous intensity in candela (cd). A candle has about 1 candela. A 100 watt light bulb has about 130 cd. So, I guess you need rather large values. :-)

By the way, your test model is quite beautiful. Is it available online? I'd like to use it in my thesis.

Cheers!

Anyway, the reason why things are getting dark is, that your light source is probably too far away. Consider a distance of 10 units in space. Squared and divided gives you 1/100 of your un-attenuated intensity. I think, things should work for you, if you make your light brighter, i.e. your variable “lightColor”. The lightColor actually stores the luminous intensity in candela (cd). A candle has about 1 candela. A 100 watt light bulb has about 130 cd. So, I guess you need rather large values. :-)

By the way, your test model is quite beautiful. Is it available online? I'd like to use it in my thesis.

Cheers!


So my light color, which is currently ( 1.0f, 1.0f, 1.0f ), needs to be something like ( 100.0f, 100.0f, 100.0f )? I tried this but it is still the same result. I am apparently missing some of the logic here. I'm not sure what you are talking about dividing by PI. I haven't seen any diffuse/specular calculations that use PI, unless you're talking about the actual physical model, not the real time phong model. I guess I should have specified tongue.png

Also my light position is at ( 5, 5, -5 ) so it's not a crazy distance away or anything. I have it coded so that I can move the light around so I should be able to move it close enough.

I got the 3d model a while ago so I have no clue where it is online but I can upload it somewhere for you (no texture, just v/vn obj file). What's a good free upload place?

Update: I can only get it to light the object if make the equation attenuation = 1.0f / ( d*d*0.00000075 ) or making the light color something ridiculous like ( 999999, 9999999, 99999999 ) but that's kind of silly, there has to be a better way right? Also, using that, I can't get the light to completely "fall off" no matter how far I move it away from the object.
Actual light does not fall off completely either.

Niko Suni

Hi again,


I'm not sure what you are talking about dividing by PI. I haven't seen any diffuse/specular calculations that use PI, unless you're talking about the actual physical model, not the real time phong model. I guess I should have specified tongue.png

The BRDF tells for an incoming direction how much light is reflected to an outgoing direction. If you sum up the BRDF over all out-going directions (i.e. integrate the BRDF over the hemisphere) it tells you how much energy is reflected in total. This shouldn’t be more than one, thus for energy conservation we just divide by that integral (making the result exactly one). For a diffuse reflection, this integral happens to be Pi.

Here are some slides on reflectance models for games, including physically plausible BRDFs (though, they approximate quite a lot).


So my light color, which is currently ( 1.0f, 1.0f, 1.0f ), needs to be something like ( 100.0f, 100.0f, 100.0f )? I tried this but it is still the same result. I am apparently missing some of the logic here.
Update: I can only get it to light the object if make the equation attenuation = 1.0f / ( d*d*0.00000075 ) or making the light color something ridiculous like ( 999999, 9999999, 99999999 ) but that's kind of silly, there has to be a better way right? Also, using that, I can't get the light to completely "fall off" no matter how far I move it away from the object.

To get back to your problem, in what spaces are light_vector and input.pos? Are they both in world space? The values you reported to see anything at all are indeed a little extreme. :)
And yes, in theory the attentuation won't fall down to zero. In practice, however, you just cut it off somewhere.


I got the 3d model a while ago so I have no clue where it is online but I can upload it somewhere for you (no texture, just v/vn obj file). What's a good free upload place?

Sounds good! Dropbox perhaps? When you have installed it, you get a "public" folder (which is synced with a server somewhere). You could copy the file in there, do a right click, select "Copy public link" and then paste the link here. What’s the size of the file?


Actual light does not fall off completely either.

The photons don’t lose energy, that’s true. But the solid angle of the receiver area (the pixel / your eye) gets smaller the farer you are away, when viewed from the light source.
Photometric distance law:
E = I * dot(N,L) / (squaredDistance)
E=irradiance, I=intensity, N=normal, L=light direction.

By the way, the other attenuation parameters (constant and linear) are just for artistic purposes. Squared attenuation would be most correct, since the irradiance of a point light is:
E = max(0, cosAngle) * vLightIntensity / (squaredDistance);
For a diffuse surface the radiance then becomes:
L = E * vSurfaceColor / Pi;
You got it right, except for the division by Pi. (It is there for the energy conservation.)

If anything it would be:
[eqn]I(\mathbf x,\omega)=L(\mathbf x,\omega){\mathbf r_0}^2\pi[/eqn]
[eqn]E(\mathbf x,\omega)=I(\mathbf x,\omega)\frac{1}{\mathbf r^2}[/eqn]
[eqn]L_o(\mathbf x,\omega)=L_e(\mathbf x,\omega)+f_r(\mathbf x,\omega_i,\omega)E(\mathbf x,\omega_i)(-\omega_i\cdot \mathbf n)[/eqn]
[eqn]L_o(\mathbf x,\omega)=L_e(\mathbf x,\omega)+f_r(\mathbf x,\omega_i,\omega)L_i(\mathbf x,\omega_i)(-\omega_i\cdot \mathbf n){(\frac{\mathbf r_0}{\mathbf r})}^2\pi[/eqn]

The [eqn]\pi[/eqn] does not appear in the shader code though, as it cancels out with most BRDF's. For example lambert: [eqn]f_r(\mathbf x,\omega_i,\omega_o)=\frac{\mathbf k_d}{\pi}[/eqn]

Whether [eqn]\pi[/eqn] needs to be in the shader code depends on the implementation. It depends on what his light is storing. It could either be radiance or radiant intensity. The way he is doing it now is storing radiant intensity, which is [eqn]I(\mathbf x,\omega)=L(\mathbf x,\omega){\mathbf r_0}^2\pi[/eqn]. Depending on whether his radiant intensity contains the multiplication by [eqn]\pi[/eqn], the division by [eqn]\pi[/eqn] needs to be in the shader code or not.
Hi CryZe!

Alright, let me show you how I got to my equations. I’m still convinced that they are correct. smile.png

The intensity is the flux per solid angle: [formula]I = \frac{\Phi}{\omega}[/formula]
The solid angle is area of a cone cap divided by squared distance: [formula]\omega=\frac{A}{r^2}[/formula]
Now, I assume the solid angle’s top sits at the point light position and opens up toward the viewed receiver area. The viewed receiver area is the receiver area *viewed* from the light’s view. Therefore, the *viewed* receiver area is the receiver area times the cosine of the enclosed angle between normal and direction to light. (That’s what makes the difference to your equation. You forgot in your second line about using only the *viewed* part.)
[formula]I=\frac{\Phi}{A \cdot cos(\theta)/r ^2}[/formula]
Now, using that the irradiance is the flux per receiver area: [formula]E=\frac{\Phi}{A}[/formula]
gives: [formula]I=\frac{E \cdot r^2}{cos(\theta)}[/formula].
Rearranging leads to: [formula]E=\frac{I \cdot cos(\theta)}{r^2}[/formula], which is my line from before.

Your first line is slightly wrong, too. From the definition of the radiance: [formula]L=\frac{\Phi}{A \cdot cos(\theta) \cdot \omega}[/formula] and the definition of intensity:[formula]I=\frac{\Phi}{ \omega}[/formula] we get: [formula]L=\frac{I}{ A \cdot cos(\theta)}[/formula]
Rearranging and using [formula]A=r_0^2\pi[/formula] gives: [formula]I=L \cdot r_0^2\pi \cdot cos(\theta)[/formula] (Your first line misses the cosine.)
You pulled the cosine in later in the third line (which is therefore wrong, too). Note that a BRDF is [formula]fr = L_i/E_i[/formula]. The cosine isn’t here as well.
Eventually, you get the correct result in the end, since your mistakes in line one and three cancelled each other out. Summarizing, your cosine should move from line three to line one and everything is fine.

As for the thing about Pi, I usually just compute the irradiance for a light like that:
[formula]E=\frac{I \cdot cos(\theta)}{r^2}[/formula]
and than just use [formula]L=fr \cdot E[/formula] to get the radiance for that light.
I don’t explicitly compute the incoming radiance ([formula]L_i[/formula]), therefore my Pi in the BRDF doesn’t cancel out.

Best regards!
Ok, thanks for the clarification smile.png
I wasn't sure when to include the cosine. I just knew that it's part of the rendering equation. Good to know that it's part of the conversion from radiance to radiant intensity. In a few articles I've read it wasn't part of the first two equations either, that's why I wondered.
Good to know though ^^
Wow, those posts are way over my head tongue.png



Hi again,

To get back to your problem, in what spaces are light_vector and input.pos? Are they both in world space? The values you reported to see anything at all are indeed a little extreme. smile.png
And yes, in theory the attentuation won't fall down to zero. In practice, however, you just cut it off somewhere.


inpus.pos is in world space (transformed in vertex shader), but the light_vector is just raw input, no transformations. Is that correct?

Here are the lighting equations I'm using, from the book "Mathematics for 3D Game Programming and Computer Graphics":

[eqn]\mathcal{K}_{diffuse} = \mathcal{DA} + \mathcal{D}(\sum_{i=1}^{n}\mathcal{C}_{i}max(\mathbf{N}\bigodot\mathbf{L}_{i},0))[/eqn]

D = diffuse reflection color
A = ambient intensity (should this be a float or a float3?)
C = light attenuation * light_color
N = surface normal
L = normalized light vector

[eqn]\mathcal{K}_{specular} = \mathcal{S}(\sum_{i=1}^{n}\mathcal{C}_{i}max( \mathbf{R} \bigodot \mathbf{V}_{i}, 0 )^m ( \mathbf{N} \bigodot \mathbf{L} > 0 ) )[/eqn]

S = specular reflection color
C = light attenuation * light_color
R = 2*( N dot L )*N - L
V = camera vector
m = specular exponent
N = surface normal
L = normalized light vector

Here is the dropbox link to the model: https://www.dropbox....pl6jls/kit2.obj

This topic is closed to new replies.

Advertisement