Repository navigation
Why shading is per fragment
Every fragment interpolates the normal, normalizes it again, and sums the lights itself. Shading at the corners and interpolating the color, which is Gouraud shading, gives a coarse sphere a crease along every edge.

A triangle shaded at its corners can only be as round as its corners. A highlight that lands between two vertices is lost. A lamp close to a surface lights the corners and nothing in between.
The normal travels to the fragment, and so does the world position. The fragment does three steps:
interpolate the normal -> make it unit length again -> sum the lights
The average of two unit vectors is shorter than either. Two normals 45 degrees apart average to a vector 0.92 long. An interpolated normal used as it is dims the middle of every triangle. The result is neither Gouraud nor Phong but a third, wrong thing.
tests/test_rasterizer.mojo pins this with a triangle whose two leaning corners each catch cos(45) of the light. The point between them catches all of it. Without the renormalization it catches cos(45) too.
Lighting.intensity_at on the host and _arriving in the kernel do the same sum. A parity test with a colored ambient and a colored lamp holds them together, so a swapped channel cannot hide.
Every earlier example is flat-faced, and for a constant normal the two methods agree. Four of seven images were byte for byte unchanged. Three differed in one byte each, by one, from a reassociated multiply. A larger change would have meant a bug.
A point light needs the fragment's position as well as its normal. That is why RasterVertex.world exists and why the GPU vertex has sixteen floats. falloff is one function that both rasterizers call.
Tutorials
How-to
Reference
- Scene graph
- Rotations
- Cameras
- Geometry
- Geometry addons
- Lofts and clipping groups
- Sculptor
- Procedural generators
- Meshes and assets
- Lines
- Points and sprites
- Helpers
- Scene objects
- Animated objects
- Raycasting
- Curves and paths
- Animation
- Skinning
- Materials
- Node materials
- TSL functions
- Lights
- Lighting addons
- Fog
- Textures
- Renderer
- Renderer hooks and material flags
- Rasterization
- GPU backend
- Render target
- Post-processing
- GPU computation
- Compute nodes
- Progressive light map
- Gaussian splats
- Voxel global illumination
- SVG renderer
- Image files
- Image decode queue
- Model files
- More model files
- Exporters
- Scene JSON
- Windowing and controls
- Math
- Norm consumers
- Math addons
- Units
- Coverage tool
- Commands
- Examples
- Benchmarks
- Extensions
- Shared physics
- Physics query snapshots
- Continuous collision
- Tick force restoration
- Water
- Animals
- Buildings
- CARLA
- Femur
- Tibia
- Fibula
- Patella
- Knee
- Muscles
- Vessels
- Lymph
- Nerves
- Integument
- Leg
- Foot
- Pelvis
- Torso
- Arm
- Hand
- Head
- Genome
- Game humanoid
- Humanoid fidelity
- Audio-aligned game faces
- Converted assets
- Mesh quality
- Segment inertia
Explanation
- Software rasterizer
- Scene graph as an array
- Linear color
- Normals are not colors
- Perspective correction
- Fixed-point coverage
- Per-fragment shading
- Mipmaps
- Sorted transparency
- Types and checks
- Render hooks as a trait
- Shared CPU and GPU code
- Bands and threads
- PNG structure
- Extensions beside the port
- CUDA teardown hang
- Mojo compiler hang