GPU instance rendering — engine v17 Use our custom GPU system for large visual populations. Unity VFX Graph is not required. Existing collections remain on the original renderer until styled. These are visual instances, not GameObjects, colliders or CS physics objects. Use the existing GPU physics bridge separately when interactions are required. Give a collection a lit material, shadows and per-instance transforms. await createGPUInstances("rocks", "sphere", 1000, {size: 0.5, extent: 200}); await styleGPUInstances("rocks", {roughness: 0.8, metallic: 0}); Set rotation in degrees and independent XYZ scale for selected instances. await setGPUInstanceTransforms("rocks", [ {rotation: {x: 0, y: 30, z: 0}, scale: {x: 2, y: 1, z: 0.7}, emission: 1} ], 0); The last argument is the first instance index. Small setup edits only; animate large populations inside WGSL. Omitted fields use identity rotation/scale and emission 1. Positive scales only. Updates are validated before applying. Keep each command below 100,000 characters; split larger setup lists. Assign browser-accessible PNG material textures (HTTPS or same-origin). await setGPUInstanceTexture("rocks", "albedo", "/models/rock-color.png"); await setGPUInstanceTexture("rocks", "normal", "/models/rock-normal.png"); await setGPUInstanceTexture("rocks", "metalRough", "/models/rock-material.png"); Use direct URLs with no redirects; cross-origin files need CORS. PNG only, up to 4 MiB and 2048x2048. Albedo is sRGB; normal and material maps are linear. Normal is ordinary tangent-space RGB (not Unity packed/DXT5). Metallic is R, roughness G; a glTF metallic-roughness map stores metallic in B, so repack first. Maps multiply scalar metallic/roughness values; set those to 1 for full map range. A failed texture replacement retains the previous texture. UVs come from the mesh. await styleGPUInstances("rocks", { tint: {x: 0.8, y: 0.9, z: 1}, metallic: 0.2, roughness: 0.55, normalStrength: 1, tiling: {x: 2, y: 2}, offset: {x: 0, y: 0} }); Settings patch the existing material; unspecified settings remain unchanged. PBR uses the scene's sun, additional lights, ambient SH and environment reflection. It does not create lights or reflection probes. Use existing CS lighting controls. Make bright stars or indicators. HDR emission supplies brightness; a visible halo also needs camera bloom, e.g. setPostProcessing(0,0,0,0.5,0). await styleGPUInstances("stars", {lit: false, surface: "additive", emission: {x: 3, y: 1.5, z: 0.5}, castShadows: false}); Emission multiplies particle RGB and the per-instance emission factor. Alpha multiplies particle alpha, material opacity and albedo texture alpha. Use cutout for leaves/fences and transparent for glasslike visual layers. await styleGPUInstances("leaves", {surface: "cutout", cutoff: 0.5}); await styleGPUInstances("mist", {surface: "transparent", opacity: 0.3, castShadows: false}); Surfaces: opaque, cutout, transparent, additive. Transparent/additive do not write depth or cast solid shadows. Instances are not depth-sorted: overlapping alpha transparency can show ordering artifacts. Prefer cutout foliage or additive stars. Billboards face MainCamera; meshes use their own UVs and normals. Double-sided. await styleGPUInstances("rocks", {castShadows: true, receiveShadows: true}); Sun/camera shadow settings must also be enabled. Off-camera casters are retained within shadowDistance. Shadows use the base mesh; disable/distance-limit them for very large populations. This is not GPU ray-traced lighting. Cull invisible instances and reduce distant mesh detail without changing IDs. await styleGPUInstances("rocks", { frustumCulling: true, maxDistance: 150, lodDistance: 25, farDistance: 60, lodMesh: "mediumSphere", farMesh: "lowSphere", shadowDistance: 60 }); LOD mesh names can be readable static scene MeshFilter objects. "mediumSphere" and "lowSphere" are built-in sphere approximations; use matching custom meshes for other shapes. Empty mesh name reuses the base mesh. Distances are in world units from MainCamera, not screen-space errors. maxDistance/shadowDistance 0 means unlimited. The collection's extent must still enclose all instance positions. Only first mesh/submesh is used, with one material. Skinned meshes are not supported. var report = await getGPURenderState("rocks"); report.render: visible, near, medium, far, shadowCasters, visibleTriangles, shadowTriangles, bufferBytes, meshBytes, textureBytes, sampled, pending, error. Counts use asynchronous readback on demand: the first report may be unsampled; read again later. pending indicates a new sample, while sampled values are the last completed sample. These are collection counts, not full-frame FPS or the complete Unity memory footprint. Ask at a modest rate (e.g. once/second). GPU programming ABI Existing Particle remains 64 bytes: positionSize, velocityMass, color, properties. Call styleGPUInstances BEFORE compileGPU/compileGPUPasses to add an optional 32-byte-per-instance styles array at storage binding 4: styles[i].rotation: quaternion x,y,z,w, identity (0,0,0,1). styles[i].scaleEmission: positive scale x,y,z and emission factor w. WGSL can update these directly, alongside particle color/position/size. For a Y rotation by angle radians: vec4(0, sin(angle/2), 0, cos(angle/2)). Already-compiled older programs keep working; recompile to access styles. Never change the Particle layout. Initialize all style fields when writing them. Renderer adds approximately 48 bytes/instance plus 96 bytes for style, visibility and indirect buffers, beyond the existing 64-byte Particle (and optional snapshot). Mesh/texture allocations also count toward the shared 512 MiB budget. Particle physics capacity is unchanged; visual capacity is not physics capacity. Lifecycle: removeGPUInstances(name) or normal clearRun frees the collection, renderer, textures and style buffers. Reconstruct from editor code for sharing. Check getGPUCapabilities().instanceMaterials / instanceTransforms / instanceLOD / instanceShadows. A stale player needs Game refresh, not merely Run. Ten independent examples: catalog/examples/gpu-renderer-*.js.