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:
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
This is the normal map produced:
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
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!




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 calculatebiTangenttwice? (3) geometry normal has front face check, while shading normal does not, hence the light leak. $\endgroup$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 excessiveVector3 biTangent ..., since you should get your biTangent solely fromcross(N, tangent), note that use N, not NA. This includes the computation oftransformedNormal. $\endgroup$