8
\$\begingroup\$

We're having some issues with our ray tracing in DirectX, especially with some serious banding issues with specular. With high specular power (above 8) banding starts. I'm wondering if this is an HDR/LDR issue or could be related to something else, such as normals or other vectors?

UPDATE

Look below for updates.

Here's the relevant shader code for Blinn-Phong on a sphere:

float3 hitPoint = thisRay.origin + thisRay.direction * bestHit.hitT;
float3 normal = normalize(hitPoint - spheres[bestHit.hitID].center);
float3 toLight = pointLights[0].position.xyz - hitPoint;
float d = length(toLight);
toLight = normalize(toLight);

float diffuse = max(dot(normal, toLight), 0.0f);

float3 v = normalize(thisRay.origin - hitPoint);
float3 h = normalize(v + toLight);

float spec = 0;
if (diffuse > 0)
    spec = pow(max(dot(normal, h), 0.0f), specPower) * diffuse;

output[threadID.xy] = spheres[bestHit.hitID].colour * diffuse + spheres[bestHit.hitID].specColour * spec;

specPower equals 8 in this image specPower equals 8 in this image

specPower equals 9 in this image specPower equals 9 in this image

Is this as simple as an HDR/LDR issue or is it somehow related to normal precision? I believe I have encountered this issue before in a deferred renderer where normals were of low precision and improperly packed/unpacked but in this case normals are generated on the fly and the whole thing is rendered directly to the backbuffer.

Update 1

I would like to add to the above that triangles suffer the same artifact and they have their normal currently generated in the following way:

float3 normal = normalize(cross(triangles[bestHit.hitID].vertices[1] - triangles[bestHit.hitID].vertices[0],
                                    triangles[bestHit.hitID].vertices[2] - triangles[bestHit.hitID].vertices[0]));

I'd say this makes it even more unlikely that the normal of the surface is the problem. The following image shows what happens when specPower reaches 2048.

enter image description here

\$\endgroup\$
  • 1
    \$\begingroup\$ just a guess: try float3 h = normalize( reflect(toLight,normal) );, and spec = pow(dot(v, h) * 0.5 + 0.5, specPower) * diffuse; \$\endgroup\$ – Raxvan Jul 17 '14 at 15:55
  • \$\begingroup\$ Sorry to say this produced the same artifact. \$\endgroup\$ – Bentebent Jul 17 '14 at 18:24
  • 4
    \$\begingroup\$ Looks like an overflow issue. Your value should go to zero, but is instead wrapping back around to 1. Try outputting the value of spec directly, and also max(dot(normal, h), 0.0f). Look for this return to a 1 value in either calculation. \$\endgroup\$ – user41442 Jul 17 '14 at 18:52
  • \$\begingroup\$ This image shows at what point spec goes above 0.9f so that seems like it could be the culprit. imgur.com/YbTS8m5 \$\endgroup\$ – Bentebent Jul 17 '14 at 19:12
  • \$\begingroup\$ Meanwhile, max(dot(normal, h), 0.0f) looks like this (imgur.com/YV22egw) when outputting values above 0.99f. \$\endgroup\$ – Bentebent Jul 17 '14 at 19:19
1
\$\begingroup\$

Looks like an issue with the pow function implementation on this particular GPU.

When suspecting a bug in a math function replace the math function by its equivalent, for example replace:

spec = pow(max(dot(normal, h), 0.0f), specPower) * diffuse;

With the bug showing up at specPower=9.0, hard-code it to

float temp = max(dot(normal, h), 0.0f);
spec = pow(temp, 8.0) * temp * diffuse;

Or

float temp = max(dot(normal, h), 0.0f);
spec = temp * temp * temp * temp * temp * temp * temp * temp * temp * diffuse;

If the issue goes away this means there's probably an underflow bug in the pow function regarding the exponent on this GPU or driver.

What you can do as a work around, assuming the fragment shader calculation is done internally with 16bit floats. If I calculate this right you should do

pow(max(temp, (Nth root of (1.0/16384.0))), N);

It could be 32768 or 8192: I might be off by one bit or the GPU might be using more or less precision. for pow(x, 9.0) the clamping would be pow(max(temp, 0.3401975), 9.0) Values of X below this would underflow the exponent range (+15 to -14) of IEEE 16bit floating points (again, I'm assuming this is what the GPU uses.)

from rosetta code ( http://rosettacode.org/wiki/Nth_root#C.2B.2B )

double NthRoot(double value, double degree)
{
    return pow(value, (double)(1 / degree));
};

my_shader->SetUniform("specular_minimum", NthRoot((1.0/16384.0), specPower));

You'll have to calculate this on the CPU and feed the value in a uniform: the GPU will probably have the same precision issue if you calculate this right in the shader.

If you can confirm this, try contacting the GPU driver team with a sample shader source code that replicate the issue give them the exact driver version, OS, and GPU model revision, an executable might help but don't send it as your email will get trapped in the virus/spam checker. Just build one and keep a copy in case they ask for a pre-built sample executable. As a courtesy, give them a chance to fix it before naming it publicly. It could simply be an optimization bug in their shader compiler that only happen with your particular shader. It might take a while (months) to get in contact with them, these guys are often understaffed.

| improve this answer | |
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.