0
\$\begingroup\$

I am writing a program that displays a bunch of 3D objects in a room with lighting. I am doing that from scratch, so I have to implement normals calculation myself.
Say I have a set of 3D points representing a 3D object. On one hand, I can calculate the normals for every surface and vertex, and then store them in memory. Then I'll need to place the object somewhere in the room, that is, add the coordinate of the object to every vertex of it, and also do the same with normals. But I've heard that you always have to normalize normals, so on the other hand, I may normalize normals, but then I won't be able to move them as easy as just adding the location of the object to them.

So, how is that implemented in real 3D games?

Are normals calculated once for every object, get saved in memory, and get normalized before calculating the angle of incidence and reflection?
OR
Are normals recalculated and normalized every time an objects changes its position?
OR
Maybe normals are not stored in memory at all, and you calculate and normalize them every time a new frame is rendered?
Or even maybe something else?..

\$\endgroup\$

1 Answer 1

1
\$\begingroup\$

You seem to have some misconceptions about how to work with mesh data.

  1. You do not add a position to a normal. Ever.

    Normals represent a facing direction, not a location. If an object translates in space, without rotating or scaling, its normal vectors remain 100% unchanged.

  2. We usually don't modify the mesh source data at all when we move objects around the scene.

    Instead, we associate each instance of a rendered mesh with its own transformation matrix (sometimes called a "Model" matrix), which expresses its translation, rotation, and scale within the scene. We can have many copies of the object, all sharing the same mesh data in memory, so each copy only needs to store a dozen(ish) floats to describe its transformation relative to the source data.

    When we want to render the object, we send its vertex data (positions, normals, texture coordinates) to the GPU as a vertex buffer, and send the transformation matrices to use in uniform variables or a separate buffer.

    The vertex shader then runs once for each vertex, and transforms the vertex position by the MVP matrix (Model - View - Projection) to map it into the screen coordinate space. This folds together the transformation of this instance of the mesh within the scene (Model) with the transformation of the camera viewpoint (View) and the projection parameters like field of view angle and near/far planes (Projection), and we can compute that combined matrix just once per object per frame and then get all three transformations applied at just the cost of a single matrix-vector multiplication per vertex.

    We'll also transform the normals (and tangents/bitangents, if applicable) in a way that ignores the translation of the model instance. One way to do this, if you don't have non-uniform scales on the object, is to just treat the normal as if its 4th component "w" was zero, and then multiply by the Model matrix to get it into world space, or by the Model-View matrix if working in view space.

    If you do have non-uniform scale, then you need to use a related matrix called the inverse transpose.

    Finally, inside the fragment shader, we normalize our normal/light/view vectors per-pixel. This compensates for any shortening that occurred when these vectors were linearly interpolated between the vertices of the triangle to the point we're shading right now.

Note that through all of this, the mesh's vertex position and normal data stored in video memory remains read-only. All our transformations work on temporaries that exist only during the execution of a shader, and are not stored anywhere.

Now if you're writing a software renderer or raytracer/path tracer that does not use the conventional GPU pipeline described above, you can still treat the mesh position/normal information as read-only. When you want to intersect a ray against a transformed instance of the mesh, first transform the ray by the inverse of the instance's transformation matrix. Now your ray is in the coordinate system of the original, untransformed mesh data, and you can do your intersection and reflection calculations on that unmodified data. Then you can take the results and transform them back to world space (or the coordinate system of your choice), using the instance's transformation matrix again.

\$\endgroup\$
2
  • \$\begingroup\$ So first I calculate all normals and normalize them, and I save them as read-only. Then if I move an object, its normals do not change, but if it gets rotated or scaled, I have to apply the same transformation matrix to normals. They may get denormalized after that, so I always should renormalize them for further calculations. Did I get it all right? \$\endgroup\$
    – Andru
    Commented Feb 5, 2022 at 17:42
  • \$\begingroup\$ If it gets rotated, you can apply the same transformation matrix (really the upper-left 3x3 portion of it) to the normals. If it gets uniformly scaled, you don't have to do anything to the normals (normalizing cancels out any uniform change in length). If the scale is non-uniform, you need to use the inverse transpose matrix instead to get correct shading. \$\endgroup\$
    – DMGregory
    Commented Feb 5, 2022 at 17:48

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .