# 3D rendering A perspective camera maps a right-handed 3D world to one viewport. World +X points right, +Y points up, and the default camera looks along -Z. Camera orientation is a quaternion in `(x, y, z, w)` order that turns camera-local coordinates into world coordinates. ```nupp const camera = tecs.gfx.newCamera3D({y = 2, z = 6, verticalFov = math.rad(60)}) const lighting = tecs.gfx.meshes.newLighting() lighting.shadows = true world:spawn(tecs.gfx.View({camera3D = camera, lighting = lighting})) ``` `View` connects a camera and lighting settings to a viewport. A mixed 2D/3D view renders meshes behind sprites; an additional 2D view can place a HUD over several 3D cameras. See [Views](views.md) and the original split-screen scene: ```sh nupp task ex-scene3d ``` Two camera views of the original cube, floor and emissive mesh. ## Models and authored geometry The [model API](tecs.gfx.models) carries the complete load, instantiate, spawn, bind and animate recipe. Each instance owns its joint palettes, morph weights and primitive templates. Copies share geometry, materials, textures, morph deltas and clips while playing independently. A drawable primitive combines `Transform3D`, `Mesh`, `Bounds3D`, `MeshMaterial`, `Tint` and `Renderable3D`. Skin and morph components add the deformation data. `models.newMesh` accepts the original twelve-float vertices: position XYZ, normal XYZ, tangent XYZW and UV. Triangle indices are zero-based. Optional RGBA vertex colors remain a separate stream. Procedural skinning supplies four joint indices and four weights per vertex; `MeshSkin` carries column-major matrices. Bounds remain caller-owned and must enclose every animated pose. Morphing runs before skinning. Animation samples the file's base transform, applies the selected clip, builds world matrices in parent order and updates the bound components and instance-owned palettes. Static instances retain their buffers. ```sh nupp task ex-gltf3d nupp task ex-skinning3d nupp task ex-morph3d nupp task ex-animated3d ``` The animated scene uses the original CC0 hero and colored morph cube. O toggles ambient occlusion; F12 saves `animated3d.png`. In every 3D demo, click to capture the pointer, use WASD to move, Q/E for height, Shift to sprint and Tab to release. The original animated hero and morph cube. ## Materials and textures Mesh materials select metallic-roughness Cook-Torrance, unlit or Lambert shading with `models.MATERIAL_*`. Lambert keeps matte direct light independent of the viewer while sharing texture, normal, occlusion, fog, shadow and local light handling with the PBR path. `models.newMaterial` authors a shared material independently of geometry. It accepts base color, normal, metallic/roughness, occlusion and emission maps. An import-time `models.load` material callback can tune a scene before upload, as the original Sponza and Bistro examples do for converted materials. Opaque and masked primitives write depth. Blended primitives use one global GPU sort across materials and geometry, back to front with entity ID breaking ties. Mirrored transforms reverse the front-face winding. Double-sided materials render both faces. Culling and indexed indirect commands remain on the GPU. RGBA images generate their mip chains on the GPU after upload. SVG images are rasterized by the asset service. Prepared BC3 KTX2 textures retain their complete compressed mip chains in GPU memory. Color and linear material maps share storage with the appropriate sampling views. ## Lighting, shadows and ambient occlusion [`Lighting`](tecs.gfx.meshes.Lighting) belongs to a view. It configures a directional source, ambient irradiance, linear camera-distance fog, shadows, SSAO and image-based lighting. `PointLight3D` and `SpotLight3D` are entities, positioned by `Transform3D`; spotlights point along its local negative Z. Lights are binned into screen tiles for each view. Directional shadows use three camera-frustum cascades. Each light-space center snaps to shadow texels so camera translation does not slide the sampling grid under stationary geometry. Opaque and masked meshes cast; all material modes receive. Cascade distance, split blending, depth padding, bias and PCF softness are adjustable. Local shadows use six faces per point light and one per spotlight. Set `lighting.localShadows = true` and give the light `LIGHT_CASTS_SHADOWS`. The configured capacity limits how many lights receive atlas slots. ```sh nupp task ex-shadows3d ``` Directional cascades and colored local shadows across the original pillar scene. SSAO reconstructs opaque positions from depth, evaluates a world-anchored hemisphere kernel and applies an edge-aware blur. It darkens only ambient light; it does not change direct light, transparent meshes or sprites. Its targets are allocated only when enabled. ## Ambient probes and environments An ambient cube holds six world-space RGB irradiance faces. Squared normal axes blend the positive/negative X, Y and Z faces. A separate six-layer mipmapped environment supplies roughness-dependent specular reflections and the sky. `models.loadEnvironment` takes face paths in +X, -X, +Y, -Y, +Z, -Z order. Reflection intensity, sky intensity and Y rotation are independent controls. ```sh nupp task ex-ibl3d ``` This restores the original gold and blue sphere rows, roughness progression, colored point lights and six CC0 SVG environment faces. Gold and blue sphere rows with the original six-face environment. ## Large scenes The original scene preparation remains a separate, cached step: ```sh nupp task fetch sponza nupp task ex-sponza3d nupp task fetch bistro nupp task ex-bistro3d ``` Fetching needs Git and curl. Bistro's optional Draco decoder also needs CMake and a C++ compiler when built for the first time. Its pinned source download is about 986 MB. Prepared files and source notices live under the ignored `assets/external` directory. The importer preserves the original pinned source revisions and prepares BC3 mip chains. Bistro's mouse wheel blends night and day and adjusts the lights attached to its emissive fixtures. Sponza with its original texture set. Bistro with fixture lights and directional shadows. ## Offscreen rendering To render a bounded run without opening a window and save the completed image: ```sh nupp task ex-animated3d --offscreen --frames 120 --screenshot animated3d.png ``` Offscreen mode uses the real GPU and advances at a fixed 1/60 second per frame. `--headless` remains a logic-only smoke run. Use the [render benchmarks](benchmarks.md) for uncapped measurements rather than timing the fixed offscreen simulation.