ScopeBee
Back to the site

ScopeBee 0.4.0

The release is about 3D and about how scenes are stored: light and shadows, thousands of objects in two draw calls, a scene format a diff can read, and a binary one for the build.

Light and shadows

A scene now has a Light component: up to four directional lights, each with its own colour and intensity. Ambient light is set in a Lighting window of its own, either one flat colour or three — sky, horizon and ground — blended along the surface normal. Lighting is computed in linear space, with tonemapping to choose from (none, PBR Neutral or ACES) and an intensity for environment reflections.

The main light casts a shadow map, hard or soft, with a strength and two biases to keep the surface free of acne. Edges are smoothed with MSAA.

A meadow of imported models under a directional light with soft shadows, and an animated fox walking through it Scenery: Nature Kit by Kenney. Fox: Khronos glTF Sample Assets

Instancing

On the web a draw call is the first thing to run out, and a thousand identical objects drawn one by one are a thousand of them. Graphics.DrawMeshInstanced draws one mesh with one material in many places at once: the matrices go into a single buffer and the GPU gets one call for all of them. An overload takes a colour per instance as well.

In the clip below there is not a single MeshRenderer in the scene. A field of 64 × 48 columns, whose heights and colours are recomputed every frame, and 120 ducks circling above it — 3,192 objects with lighting and shadows — take two draw calls, as the counter in the corner shows: one for the columns, one for the ducks. Instancing is a module of its own in the build settings, so a project that does not use it does not carry it.

3,072 columns and 120 ducks in two DrawMeshInstanced calls: Drawcalls: 2 in the corner Duck: Khronos glTF Sample Assets

Scenes a diff can read

Scenes and prefabs used to be saved as one line of minified JSON. Git saw every save as the whole file changing, and object ids were handed out afresh on each save, so inserting one object renumbered every object after it. Now a file is still JSON, but every field sits on a line of its own:

[
{"fileID":2000001,
 "type":"GameObject",
 "Name":"Sun",
 "Components":[{"fileID":2000002},{"fileID":2000003}],
 "transform":{"fileID":2000002}},
{"fileID":2000003,
 "type":"Light",
 "_type":1,
 "_color":{"r":1.0,"g":0.92,"b":0.8,"a":1.0},
 "_intensity":1.2,
 "_shadows":2,
 "_enabled":true}
]

Changing a field is one line in the diff, a new object is only its own lines, and two people editing different fields of one component merge without a conflict. An id now lives on the object and stays with it from save to save; new ones are random 64-bit numbers, so two branches that each added an object cannot hand out the same id. Floats are written as floats — 0.92 rather than 0.9200000166893005. Old single-line files open as they are, and Resave All Scenes and Prefabs rewrites a project in one go.

The build reads binary

Text is for the editor and for git; the game does not need it. When a build is made, every scene and prefab is baked into a binary file next to its source, and that is what the game loads. Field names are matched against the type once per shape of object instead of once per field of every object, values are read in sequence, and Instantiate creates a prefab straight from the binary without a round trip through a JSON document. One file serves every platform: integers are stored without a fixed width, so the same file reads on wasm and on desktop.

This is a win in load time and memory, not in traffic: compressed, the binary and the JSON weigh about the same. The editor still keeps and edits the text.

Also in this release

  • A mesh from code, two ways: vertices, triangles and RecalculateNormals, or SetData, which uploads at once and keeps no CPU copy.
  • Sprites and tiles in the world read the depth of meshes: a mesh hides a sprite behind it, while sprites keep their order among themselves.
  • On import each mesh keeps only the vertices it uses: exporters often share one buffer between many parts, and every part used to carry a copy of all of it.
  • The Unity project converter brings over MeshFilter and MeshRenderer along with the models.