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.
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 and the original split-screen scene:
nupp task ex-scene3d
Models and authored geometry#
The model API 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.
nupp task ex-gltf3d
nupp task ex-skinning3d
nupp task ex-morph3d
nupp task ex-animated3dThe 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.

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 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.
nupp task ex-shadows3d
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.
nupp task ex-ibl3dThis restores the original gold and blue sphere rows, roughness progression, colored point lights and six CC0 SVG environment faces.

Large scenes#
The original scene preparation remains a separate, cached step:
nupp task fetch sponza
nupp task ex-sponza3d
nupp task fetch bistro
nupp task ex-bistro3dFetching 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.


Offscreen rendering#
To render a bounded run without opening a window and save the completed image:
nupp task ex-animated3d --offscreen --frames 120 --screenshot animated3d.pngOffscreen 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 for uncapped measurements rather than timing the fixed offscreen simulation.