{"id":13731463,"url":"https://github.com/graphitemaster/normals_revisited","last_synced_at":"2026-01-18T21:04:42.405Z","repository":{"id":142016523,"uuid":"198048965","full_name":"graphitemaster/normals_revisited","owner":"graphitemaster","description":"revisiting a known normal transformation in computer graphics","archived":false,"fork":false,"pushed_at":"2019-07-21T13:07:02.000Z","size":44,"stargazers_count":320,"open_issues_count":2,"forks_count":8,"subscribers_count":7,"default_branch":"master","last_synced_at":"2025-10-10T13:46:57.533Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":null,"has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/graphitemaster.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2019-07-21T11:37:43.000Z","updated_at":"2025-07-06T14:33:02.000Z","dependencies_parsed_at":"2023-07-07T07:45:45.650Z","dependency_job_id":null,"html_url":"https://github.com/graphitemaster/normals_revisited","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/graphitemaster/normals_revisited","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/graphitemaster%2Fnormals_revisited","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/graphitemaster%2Fnormals_revisited/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/graphitemaster%2Fnormals_revisited/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/graphitemaster%2Fnormals_revisited/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/graphitemaster","download_url":"https://codeload.github.com/graphitemaster/normals_revisited/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/graphitemaster%2Fnormals_revisited/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":28550503,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-01-18T20:59:07.572Z","status":"ssl_error","status_checked_at":"2026-01-18T20:59:02.799Z","response_time":98,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.5:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-08-03T02:01:30.716Z","updated_at":"2026-01-18T21:04:42.400Z","avatar_url":"https://github.com/graphitemaster.png","language":null,"funding_links":[],"categories":["Maths"],"sub_categories":[],"readme":"# The details of transforming normals\n\nHave you ever seen `transpose(inverse(M)) * normal` in code before when transforming normals?\n\nThis is the defacto solution to dealing with non-uniform scale or skewed models when transforming normals and it's such an accepted practice that nearly every single graphics programming resource mentions and encourages it. The problem is it's _wrong_.\n\n## How did we get here?\nA geometric normal is fully defined by its orientation with respect to a surface and the fact that it's orthogonal / perpendicular to the tangent plane at the surface point.\n\nWhen transforming a normal we want something that preserves both of those constraints. The inverse transpose matrix used to transform the normal is derived from satisfying just the latter. That is, the **dot product** should equal zero. What we should be using instead is the **cross product** as it enforces both.\n\nNot easily persuaded? Take this very simple triangle\n\n![](tex/img2.png)\n\nNow reflect it around the `zy` axis. You'll note that the normal does not reflect properly if `transpose(inverse(M))` is used to transform it. In fact, the normal points in the culled direction. Now all your lighting calculations end up as\n\n```glsl\nmax(0, dot(n, l)) = 0\n```\n\nLets take a look at the derivation with the **dot product** to see what is actually happening here\n\n![](tex/img3.png#)\n\nWe arrive to that by using this identity\n\n![](tex/img1.png#)\n\nFrom the above we see that we need `transpose(inverse(M))`\nIn front of `N` in order to preserve orthogonality.\n\n![](tex/img4.png#)\n\nWhat is not so obvious here is that\n\n![](tex/img5.png#)\n\nHas more than one solution and orientation isn't considered. This is easy to show.\n\n![](tex/img6.png#)\n\nWhen the normal is not zero `(|N| != 0)` and the triangle isn't degenerate `(|V| != 0)` you get\n\n![](tex/img7.png#)\n\nWhich means when you have `M` such that\n\n![](tex/img8.png#)\n\nThen you will get a normal that points in the completely opposite direction.\n\n## Looking at it a bit differently\n\nSo what happens if we derive the normal from the **cross product** of three vertices instead?\n\n![](tex/img9.png#)\n\nThen we apply `M` to the vertices to get something more interesting\n\n![](tex/img10.png#)\n\nWhere `cof` here is the [_cofactor_](https://en.wikipedia.org/wiki/Minor_(linear_algebra))\n\nThis tells us something rather interesting\n\n![](tex/img11.png#)\n\nThe `transpose(inverse(M))` is missing the _sign_ from `det(M)` and that's why the normal is oriented the wrong way when `det(M) \u003c 0`.\n\nWhat we're actually interested in using is the cofactor instead of `transpose(inverse(M))`, which has the added benefit of being more efficent to compute and more accurate.\n\n## Insight to be had\nWe should **not** be deriving the normal from the **dot product** because it leads to precisely this problem. Derive it from the **cross product** and teach the derivation of it using the **cross product**.\n\n## Sample code\nIncluded here is some sample C code for calculating the cofactor of a 4x4 matrix which can be used instead of `transpose(inverse(M))`\n\n```c\nfloat minor(const float m[16], int r0, int r1, int r2, int c0, int c1, int c2) {\n  return m[4*r0+c0] * (m[4*r1+c1] * m[4*r2+c2] - m[4*r2+c1] * m[4*r1+c2]) -\n         m[4*r0+c1] * (m[4*r1+c0] * m[4*r2+c2] - m[4*r2+c0] * m[4*r1+c2]) +\n         m[4*r0+c2] * (m[4*r1+c0] * m[4*r2+c1] - m[4*r2+c0] * m[4*r1+c1]);\n}\n\nvoid cofactor(const float src[16], float dst[16]) {\n  dst[ 0] =  minor(src, 1, 2, 3, 1, 2, 3);\n  dst[ 1] = -minor(src, 1, 2, 3, 0, 2, 3);\n  dst[ 2] =  minor(src, 1, 2, 3, 0, 1, 3);\n  dst[ 3] = -minor(src, 1, 2, 3, 0, 1, 2);\n  dst[ 4] = -minor(src, 0, 2, 3, 1, 2, 3);\n  dst[ 5] =  minor(src, 0, 2, 3, 0, 2, 3);\n  dst[ 6] = -minor(src, 0, 2, 3, 0, 1, 3);\n  dst[ 7] =  minor(src, 0, 2, 3, 0, 1, 2);\n  dst[ 8] =  minor(src, 0, 1, 3, 1, 2, 3);\n  dst[ 9] = -minor(src, 0, 1, 3, 0, 2, 3);\n  dst[10] =  minor(src, 0, 1, 3, 0, 1, 3);\n  dst[11] = -minor(src, 0, 1, 3, 0, 1, 2);\n  dst[12] = -minor(src, 0, 1, 2, 1, 2, 3);\n  dst[13] =  minor(src, 0, 1, 2, 0, 2, 3);\n  dst[14] = -minor(src, 0, 1, 2, 0, 1, 3);\n  dst[15] =  minor(src, 0, 1, 2, 0, 1, 2);\n}\n```","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fgraphitemaster%2Fnormals_revisited","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fgraphitemaster%2Fnormals_revisited","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fgraphitemaster%2Fnormals_revisited/lists"}