2
$\begingroup$

I have been working on a Path Tracer ~ following the Peter Shirley Series

I decided to add "normal map" feature to the same, Normal Map Tutorial is the tutorial I followed. But the output I got is the following:

enter image description here

It seems wrong, the reason being ~ the plane is situated at ( 0, 0, 0 ) and the light is at ( 0, 0.7, 0 ). The light should be uniformly lighting the plane, but it just doesn't happen. The worse is the underside get's lit

enter image description here

This is the normal map produced:enter image description here That's when I exchange the RGB to RBG, right when we read the pixel color from raw normal map, if we don't then the normal map is more bluish. But in either case the illumination is not proper

I am not really sure what's wrong, either I am not converting the normal map ( tangent space ) to world space correctly or the shading logic is wrong.

A little about codebase:

  • Followed Peter Shirley series till book 2
  • Using ASSIMP to load GLB with embedded diffuse / normal texture
  • Use GetEmbeddedTexture() from ASSIMP to access embedded texture and then load it from memory into a 2d array using stbi_loadf_from_memory()
  • Plane + Normal Texture ( Non - Color Space + UV Map in Normal Mode ) + Diffuse Texture, exported with +Y UP
  • Peter Shirley Ray Tracer uses +Y as UP

The following code is responsible for reading RGB from normal map, convert using TBN and forward the vector to shading code

bool hit( const Ray &ray, Interval interval, IntersectionManager &intersectionManager ) const override { double denominator = dot( faceNormalizedNormal, ray.direction() ); if( std::fabs( denominator ) < 1e-8 ){ return false; } double t = ( D - dot( faceNormalizedNormal, ray.origin().toVector() )) / denominator; if( !interval.contains( t ) ){ return false; } Point3 intersectedPoint = ray.at( t ); Vector3 intersectedPointVector = intersectedPoint - A; double beta = dot(cross(intersectedPointVector, AC), faceNormal) / denom; double gamma = dot(cross(AB, intersectedPointVector), faceNormal) / denom; double alpha = 1 - beta - gamma; Vector3 N = unitVector( NormalA * alpha + NormalB * beta + NormalC * gamma ); //Also returns the interpolated UV coordinates and stores in intersectionManager if( !inShape( alpha, beta, intersectionManager ) ){ return false; } Color3 rgbNormal = material -> getNormalTexture() -> value( intersectionManager.u, intersectionManager.v, intersectedPoint ); Vector3 worldNormal = unitVector( Vector3( rgbNormal.r(), rgbNormal.g(), rgbNormal.b() ) * 2.0 - 1.0 ) ; Vector3 edge1 = B - A; Vector3 edge2 = C - A; Vector3 deltaUV1 = UVB - UVA; Vector3 deltaUV2 = UVC - UVA; float f = 1.0f / (deltaUV1.x() * deltaUV2.y() - deltaUV2.x() * deltaUV1.y()); Vector3 tangent = Vector3( f * (deltaUV2.y() * edge1.x() - deltaUV1.y() * edge2.x()), f * (deltaUV2.y() * edge1.y() - deltaUV1.y() * edge2.y()), f * (deltaUV2.y() * edge1.z() - deltaUV1.y() * edge2.z()) ); Vector3 biTangent = Vector3( f * (-deltaUV2.x() * edge1.x() + deltaUV1.x() * edge2.x()), f * (-deltaUV2.x() * edge1.y() + deltaUV1.x() * edge2.y()), f * (-deltaUV2.x() * edge1.z() + deltaUV1.x() * edge2.z()) ); tangent = unitVector( tangent ); biTangent = unitVector( cross(NormalA, tangent) ); worldNormal = Vector3( worldNormal.x(), worldNormal.z(), -worldNormal.y() ); Vector3 transformedNormal = Vector3( worldNormal.x() * tangent.x() + worldNormal.y() * biTangent.x() + worldNormal.z() * NormalA.x(), worldNormal.x() * tangent.y() + worldNormal.y() * biTangent.y() + worldNormal.z() * NormalA.y(), worldNormal.x() * tangent.z() + worldNormal.y() * biTangent.z() + worldNormal.z() * NormalA.z() ); transformedNormal = unitVector( transformedNormal ); intersectionManager.t = t; intersectionManager.point = intersectedPoint; intersectionManager.material = material; bool frontFace = dot( ray.direction(), N ) < 0; intersectionManager.frontFace = frontFace; intersectionManager.normal = frontFace ? N : -N; intersectionManager.shadingNormal = transformedNormal; return true; } 

This is the accompanying shading logic, still experimenting with it, so you will see a lot of comments

 Vector3 sample( Vector3 normal, Vector3 shadingNormal ) const { // if(dot( shadingNormal, normal) < 0){ // shadingNormal = -shadingNormal; //} Vector3 scatterDir = unitVector( shadingNormal + generateRandomUnitVector() ); // if(dot(scatterDir, normal) < 0) { // scatterDir = shadingNormal; // } return scatterDir; } bool scatter( const Ray &ray, Color3 &attenuation, Ray &scattered, IntersectionManager &intersectionManager ) const override { Vector3 sampleDirection = sample( intersectionManager.normal, intersectionManager.shadingNormal ); if( sampleDirection.nearZero() ){ sampleDirection = intersectionManager.normal; } Color3 evaluateBRDF = evaluate( intersectionManager, ray.direction(), sampleDirection ); double pdf = getPDF( intersectionManager.normal, sampleDirection ); scattered = Ray( intersectionManager.point, sampleDirection, ray.time() ); // attenuation = evaluateBRDF * dot( intersectionManager.shadingNormal, sampleDirection ) / pdf; attenuation = texture->value(intersectionManager.u, intersectionManager.v, intersectionManager.point); return true; } 

At this point I have spent 3-4 days on the same but at the end of the wits now! Please one of the smart people help me :(

I tried the following:

  • Converting RGB to RBG the moment i read from normal texture
  • Flipping the Green Channel
  • Playing around with the shading logic
  • In shading logic, currently we just return the diffuse color directly, instead of BRDF for simplicity reason
  • The funny thing is: a simple diffuse shader uses the exact same logic, use the geometric normal and add a random unit vector to it and it just uniformly lits the plane but here if we just exchange the geometric normal with pertubed normal from map, it starts producing patches

The weird thing is, when i move the light to (-1,0.7,-1) the plane is more uniformly lit ~ even for the farthest corner

enter image description here

This is the kind of output we would get for a dark pixel in image:

1st depth

UV Interpolated: 0.0905732 - 0.118825

normal map: 0.219608 0.196078 0.992157

UVA: 0 0 0

UVB: 1 1 0

UVC: 0 1 0

shading normal: -0.436197 0.472801 -0.765632

new scatter direction: -0.547091 0.808844 0.215554 ( completely away from light, most samples )

attenuation value: 0.0941176 0.0588235 0.0352941 ( already black )

2nd depth

no hit

Either the implementation is wrong? or I am not handling the scattered rays properly?

EDIT: Solved!

Normal Map

$\endgroup$
5
  • 1
    $\begingroup$ Can you clarify on these points: (1) why is the loaded normal called 'world normal'? Isn't the normal map storing normal vectors in tangential space? I suppose you want to transform from local to world, instead of doing it the other way around? (2) What's biTangent = unitVector( cross(NormalA, tangent) ); doing? Shouldn't you construct bitangent vector by the interpolated normal instead of normal of vertex A, and why did you calculate biTangent twice? (3) geometry normal has front face check, while shading normal does not, hence the light leak. $\endgroup$ Commented Oct 2 at 16:51
  • $\begingroup$ @Enigmatisms (1) Yes, the normal is stored in tangential space, and the goal is to convert from tangential space to world space, just naming is wrong on my side. (2) From what I understood, tangent, bitangent and normal are supposed to be orthogonal to each other. Tangent and normal can be made orthogonal using gram schmidt ( i didn't do it here ) and using cross product we make sure that the biTangent is orthogonal to them both. I was experimenting with different approaches initially it wasn't twice. (3) Got it, somehow even if i flip the shading normal, still the plane wouldn't be fully lit. $\endgroup$ Commented Oct 2 at 17:29
  • $\begingroup$ From what I see, you should try this: (1) tangent = tangent - dot(tangent, N) * N;, this will make your tangent perp to your shading normal. The current code you have simple computes a geometry tangent that lies in exactly the same plane with your triangle edges, so I believe it is useless. (2) Remove excessive Vector3 biTangent ..., since you should get your biTangent solely from cross(N, tangent), note that use N, not NA. This includes the computation of transformedNormal . $\endgroup$ Commented Oct 3 at 3:46
  • $\begingroup$ Hello @Enigmatisms, thank you! From your comment i did gram schmidt, calculated biTangent using cross. Another major reason why it was not working, was that stbi was applying gamma correction to the floats which messed up the normals being read. Added the output image at the end of the post. Thank You for the help! $\endgroup$ Commented Oct 3 at 17:40
  • 2
    $\begingroup$ Did you solve this problem? If so, you are encouraged to summarize the major points with an answer, instead of updating your question post, which is clearer for other users. You can accept your own answer. Take your time! $\endgroup$ Commented Oct 4 at 5:18

0

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.