PropBatch
Many copies of one mesh in a single draw call. Instancing has worked at the Mesh level since it landed -- SetInstanceMatrices and DrawInstanced, with Shader->LitTexturedInstanced to read the per-instance matrices. What was missing was any route to it from the object layer: Scene and Prop both set an "mvp" uniform per object, and the instanced shader has no "mvp", so pointing a Scene at it drew nothing at all. This is that route. A batch is one mesh, one material, many placements: 400 trees, one draw call, one uniform upload. ## One mesh, one material -- not a limitation of this class That is what the hardware does. Every instance runs the same program over the same vertices and the only thing that varies is the per-instance matrix, so a batch cannot hold two different meshes or give one tree different bark. Two kinds of tree are two batches. Worth stating plainly, because the obvious expectation of a thing called a batch is a container that accepts anything. ## Static versus dynamic The transforms are objects the caller keeps and can move at any time, and nothing tells the batch when that happens. So by default the matrices are repacked and re-uploaded every frame, which is always correct and still costs exactly one draw call -- the saving that matters most. A batch that does not move should say so with SetDynamic(false): it then packs once and never again, and the caller calls MarkMoved() if that ever stops being true. Getting that wrong is visible immediately -- the batch renders at its old placements -- rather than subtly, which is why the fast path is the one you opt into rather than the default. ## Culling is all or nothing GetBoundsRadius covers every instance, so a batch is either wholly submitted or wholly skipped. Per-instance culling would mean repacking and re-uploading the buffer whenever visibility changed, which costs more than the draw calls it saves. Batch things that are near each other; a batch spread across the whole world is never off screen and so never culls.
Example
trees := PropBatch->New(trunk_mesh);
trees->SetMaterial(bark);
for(i := 0; i < 400; i += 1;) {
trees->Add(x, 0.0, z);
};
trees->SetDynamic(false);
...
trees->Draw(camera->GetViewProjection());Operations
- New
- Add
- Clear
- Draw
- Free
- Get
- GetBoundsRadius
- GetBoundsX
- GetBoundsY
- GetBoundsZ
- GetCamera
- GetCount
- GetError
- GetLight
- GetLightRig
- GetMaterial
- GetMesh
- GetShader
- IsAdditive
- IsDynamic
- IsOk
- IsVisible
- MarkMoved
- Pack
- SetAdditive
- SetCamera
- SetDynamic
- SetLight
- SetLightRig
- SetMaterial
- SetVisible
Add #
Place another instance.
method : public : Add(transform:Transform) ~ NilParameters
| Name | Type | Description |
|---|---|---|
| transform | Transform | where it goes; held, not copied, so moving it later moves the instance |
Add #
Place another instance at a point, unrotated and unscaled.
method : public : Add(x:Float, y:Float, z:Float) ~ TransformParameters
| Name | Type | Description |
|---|---|---|
| x | Float | where to put it |
| y | Float | where to put it |
| z | Float | where to put it |
Return
| Type | Description |
|---|---|
| Transform | the new transform, to turn or scale it |
Clear #
Drop every instance. The mesh, shader and material are untouched.
method : public : Clear() ~ NilDraw #
Draw every instance, using the batch's own light.
method : public : Draw(view_projection:Float[]) ~ NilParameters
| Name | Type | Description |
|---|---|---|
| view_projection | Float | the camera matrix |
Draw #
Draw every instance under a light supplied by the caller. This is what Scene calls, so a batch added to a lit scene does not have to be told about the light twice. A batch with its own light or rig keeps it: the arguments are what to use when the batch has none.
method : public : Draw(view_projection:Float[], light:Light, rig:LightRig) ~ NilParameters
| Name | Type | Description |
|---|---|---|
| view_projection | Float | the camera matrix |
| light | Light | a light to fall back on, or Nil |
| rig | LightRig | a rig to fall back on, or Nil; wins over the light |
Free #
Release the shader, if this batch built it. The mesh is never freed here: it is the caller's, and is very often shared with a Prop or another batch. Idempotent.
method : public : Free() ~ NilGet #
method : public : Get(index:Int) ~ TransformParameters
| Name | Type | Description |
|---|---|---|
| index | Int | which instance |
Return
| Type | Description |
|---|---|
| Transform | its transform, or Nil if the index is out of range |
GetBoundsRadius #
Radius of a sphere containing every instance. Covers the instance PLACEMENTS plus the mesh's own extent at the largest scale any instance uses, so it holds however the instances are turned.
method : public : GetBoundsRadius() ~ FloatReturn
| Type | Description |
|---|---|
| Float | the radius in world units, or 0 if nothing has been packed yet |
GetBoundsX #
Centre of a sphere containing every instance, on the x axis. Valid once the batch has packed, which happens on its first draw. Zero before then -- which is why Scene tests a batch's visibility only after it has drawn at least once.
method : public : GetBoundsX() ~ FloatReturn
| Type | Description |
|---|---|
| Float | the x coordinate |
GetBoundsY #
method : public : GetBoundsY() ~ FloatReturn
| Type | Description |
|---|---|
| Float | the y coordinate of the bounding sphere's centre |
GetBoundsZ #
method : public : GetBoundsZ() ~ FloatReturn
| Type | Description |
|---|---|
| Float | the z coordinate of the bounding sphere's centre |
GetCount #
method : public : GetCount() ~ IntReturn
| Type | Description |
|---|---|
| Int | how many instances the batch holds |
GetError #
What went wrong, if anything. Empty when nothing has.
method : public : GetError() ~ StringReturn
| Type | Description |
|---|---|
| String | the last error, or an empty string |
GetMaterial #
method : public : GetMaterial() ~ MaterialReturn
| Type | Description |
|---|---|
| Material | the material, or Nil |
GetShader #
method : public : GetShader() ~ ShaderReturn
| Type | Description |
|---|---|
| Shader | the program this batch draws with |
IsAdditive #
method : public : IsAdditive() ~ BoolReturn
| Type | Description |
|---|---|
| Bool | whether this batch draws additively |
IsDynamic #
method : public : IsDynamic() ~ BoolReturn
| Type | Description |
|---|---|
| Bool | whether the batch repacks every frame |
IsOk #
Is the batch usable -- a live mesh and a live shader.
method : public : IsOk() ~ BoolReturn
| Type | Description |
|---|---|
| Bool | true when it can draw |
MarkMoved #
A transform changed and the batch is not dynamic; repack on the next draw. Harmless on a dynamic batch, which repacks regardless.
method : public : MarkMoved() ~ NilNew # constructor
A batch drawing the given mesh with a built-in instanced shader. The shader is created and OWNED here, so Free releases it. Use the two-argument constructor to supply your own, which is then the caller's to free.
New(mesh:Mesh)Parameters
| Name | Type | Description |
|---|---|---|
| mesh | Mesh | the geometry every instance draws; NOT owned by the batch |
New # constructor
A batch drawing the given mesh with a shader you supply. The shader must read the per-instance model matrix from attribute locations 3 to 6 and take the camera through a "view_projection" uniform. Shader->LitTexturedInstanced is the built-in one; anything with an "mvp" uniform is the ordinary per-object shape and will not work here.
New(mesh:Mesh, shader:Shader)Parameters
| Name | Type | Description |
|---|---|---|
| mesh | Mesh | the geometry; NOT owned by the batch |
| shader | Shader | an instanced program; NOT owned by the batch |
Pack #
Repack the matrices, upload them, and recompute the bounding sphere. The bounds are computed here rather than in a method of their own because both walk the same transforms, and walking a thousand of them twice is the sort of thing this class exists to avoid.
method : private : Pack() ~ NilSetAdditive #
Draw this batch with ADDITIVE blending, and no depth writes. For anything that emits light rather than blocks it: sparks, fire, glows, muzzle flashes, magic. Colours accumulate, so bright places get brighter and black contributes nothing -- which is why a sprite sheet for additive work has a black background rather than a transparent one. The reason this is the blending mode offered here and ordinary transparency is not: addition COMMUTES, so additive geometry needs no back-to-front sort. Ordinary alpha blending does, and a batch is one draw call over an instance buffer with no per-instance ordering in it -- so offering it would mean offering something that renders wrongly whenever two sprites overlap.
method : public : SetAdditive(additive:Bool) ~ NilParameters
| Name | Type | Description |
|---|---|---|
| additive | Bool | true for additive blending |
SetCamera #
The camera whose basis a billboard shader needs. Only read by a shader that declares a "view" uniform -- Shader->BillboardInstanced does; nothing else here does. Harmless otherwise.
method : public : SetCamera(camera:Camera) ~ NilParameters
| Name | Type | Description |
|---|---|---|
| camera | Camera | the eye, or Nil |
SetDynamic #
Whether the matrices are repacked every frame. True by default, which is always correct. False packs once and is the right answer for anything that does not move -- scenery, a grid, a crowd that stands still -- but then a transform changed afterwards has no effect until MarkMoved is called.
method : public : SetDynamic(dynamic:Bool) ~ NilParameters
| Name | Type | Description |
|---|---|---|
| dynamic | Bool | true to repack every frame |
SetLight #
Light the batch. A batch drawn through a Scene inherits the scene's light when it has none of its own.
method : public : SetLight(light:Light) ~ NilParameters
| Name | Type | Description |
|---|---|---|
| light | Light | the light, or Nil |
SetLightRig #
Light the batch with a rig, which wins over a single light.
method : public : SetLightRig(rig:LightRig) ~ NilParameters
| Name | Type | Description |
|---|---|---|
| rig | LightRig | the rig, or Nil |