# SETech Scene Authoring — Agent Guide (static scene files) This document lets **any agent** author a **complete SETech scene as a single JSON file** that the engine can load directly — a whole level, lit, scripted, and playable, in one artifact. No running app or command bridge is required to *produce* it. > **Two different mechanisms, two different JSON shapes — do not mix them:** > > | | **This guide** (scene file) | `RemoteControl_AgentGuide.md` (live bridge) | > |---|---|---| > | Purpose | author a whole scene offline, load it | mutate a *running* scene incrementally | > | Transport | a `.json` file loaded via **Load** / `-scene` | write `ai_command.json`, app consumes it | > | Placement | **`m_Transform` matrix** in `members` | friendly `position`/`scale`/`rotationAxis` keys | > | Booleans | `"True"` / `"False"` | `"true"` / `"false"` | > | Top wrapper | `{ "world": {...}, "entities": [...] }` | `{ "clear", "remove", "edit", "world", "entities", "mode" }` | > | Verbs | none — it *is* the whole scene | `clear` / `remove` / `edit` / `mode` | > > The two share the **entity member names, enum spellings, and vector/color formats** — those come > from `ai_schema.md` and are identical. Only the wrapper and placement differ. --- ## 1. How a scene file is loaded The file describes the entire world; loading it **replaces** the current scene (skybox, lights, props, everything). Two ways in: 1. **Editor → `Scene Entities` tab → `Load`**, pick the `.json`. (`Save` / `Save As` write this same format back out — the canonical way to see a correct example is to author a scene in the editor and Save it.) 2. **Startup switch:** `SETechStudio.exe -scene myscene.json` (working dir = `Runtime`). Absent the switch the app loads `minimal_scene.json` (or `default_scene.json` with `-showcase`). Loader path (for reference): `SETechStudio::LoadDefaultScene` → `SEJSONReader::Parse` → the `world` member is applied via `SESceneWorld::LoadJSON`, then `SESceneLoader::LoadEntitiesJSON` builds each entity by reflection. --- ## 2. Top-level structure ```json { "world": { "type": "SESceneWorld", "members": { "m_strPlayerCameraName": "cam" } }, "entities": [ { "type": "", "name": "", "members": { ... } }, ... ] } ``` - **`world`** — reflected `SESceneWorld` gameplay properties. Today the only one that matters is `m_strPlayerCameraName`: the `m_strEntityName` of the camera entity to render from **while Playing** (the editor camera resumes on Stop). Leave `members` `{}` if you don't need it. - **`entities`** — the full ordered list. Each element is one entity object. --- ## 3. Entity object shape ```json { "type": "SEProceduralMeshEntity", // reflected class name (see ai_schema.md for the full list) "name": "player", // top-level display id "members": { "m_Transform": "{...}{...}{...}{tx,ty,tz,1}", // placement — see §4 "m_strEntityName": "player", // REQUIRED for FindEntityByName / player camera targeting ... reflected members ... } } ``` ### Naming (get this right or scripts/cameras can't find entities) - `FindEntityByName(...)` and `world.m_strPlayerCameraName` resolve against **`m_strEntityName`**, NOT the top-level `name`. - The loader copies the top-level `name` into `m_strEntityName` *only if* `members` didn't set one. - **Safest rule: set `m_strEntityName` = `name` on every entity you reference.** ### Members are reflected — the schema is authoritative `ai_schema.md` (written next to the running app when Remote Control is enabled, or via the AI Scene tab) lists **every creatable type**, its exact member names, enum options, ranges, nested sub-objects (e.g. `m_aParts` → `SEProceduralShapePart` fields), and the `// script API` methods. Treat it as the source of truth for *which* members exist; this guide is the source of truth for *how to format* them in a scene file. Include the base members every entity has: `m_Transform`, `m_bFrustumCullable`, `m_strEntityName`, `m_strScriptFile`, `m_strScriptClass`, `m_bScriptEnabled` (+ `m_strScriptSource` when scripting). --- ## 4. Value formats (the serializer contract) Every value is a **JSON string** (never a bare number, bool, or nested object for scalars/vectors): | Kind | Format | Example | |------|--------|---------| | float / int | quoted number | `"1.5"`, `"64"` | | bool | `"True"` / `"False"` | `"True"` — parsing is case-insensitive on `true`, but Save emits `True`/`False`; match it | | enum | quoted option spelling from the schema | `"Point"`, `"Center"`, `"Box"` | | `SEVec2` | `"{ x , y }"` | `"{ 40 , -55 }"` | | `SEVec3` | `"{ x , y , z }"` | `"{ 0 , 1 , 0 }"` | | `SEColor3` (rgb 0..1) | `"{ r , g , b }"` | `"{ 0.9 , 0.25 , 0.2 }"` | | `SEColor4` (rgba 0..1) | `"{ r , g , b , a }"` | `"{ 1 , 1 , 1 , 1 }"` | | pointer ref (e.g. physics) | `{ "$ref": null }` | leave null unless wiring a named object | | array (e.g. `m_aParts`) | JSON array of `{ "type", "members" }` objects | see §7 | ### The transform matrix — `m_Transform` Placement lives in **`m_Transform`**, a 4×4 matrix serialized as **four brace-groups, one per row**, row-major, with the **translation in the 4th row**: ``` "{ Xx,Xy,Xz,0 }{ Yx,Yy,Yz,0 }{ Zx,Zy,Zz,0 }{ tx,ty,tz,1 }" ``` Rows 0–2 are the entity's local **X / Y / Z basis vectors** (scaled); row 3 is the world position. (No spaces are needed inside; the sample scenes write full `%f`. Both parse.) **Recommended pattern — translation only.** For almost everything, keep the basis identity and put the position in row 4. This is the least error-prone thing an agent can emit: ``` "m_Transform": "{1,0,0,0}{0,1,0,0}{0,0,1,0}{ 11 , 2 , 0 , 1 }" // at (11,2,0) ``` Do **rotation/scale of geometry per-part** instead of in the entity matrix — `SEProceduralShapePart` has `m_Size`, `m_RotationAxis`, `m_RotationDegrees`, `m_Offset` (see §7). That keeps the entity matrix trivial and avoids hand-building rotation rows. **Advanced — uniform scale `s` + translation:** `{s,0,0,0}{0,s,0,0}{0,0,s,0}{tx,ty,tz,1}`. **Rotation about Y by θ:** row0 `{cosθ,0,-sinθ,0}`, row2 `{sinθ,0,cosθ,0}`. Prefer per-part rotation unless the *whole* entity (mesh + script origin) must rotate. > **Note:** the friendly `position` / `scale` / `rotationAxis` / `rotationDegrees` keys from the > remote-control guide **do not exist in scene files** — they are a bridge convenience. In a scene > file, placement is `m_Transform` only. --- ## 5. Coordinate system & camera (verified from engine math) - **Axes:** right-handed data, **+Y up**, units are metres. Ground plane is XZ; height is Y. - **View matrix is left-handed** (`SEMat4::SetLookAt`: `xAxis = up × zAxis`, `zAxis = at − eye`). - **A `SECameraSceneEntity` looks along its +local Z** (`ApplyToCamera` sets `At = Loc + localZ`). With an identity transform, forward is **+Z**. - To view a scene sitting around the origin, place the camera on the **−Z side looking toward +Z** — e.g. position `(0, 2.5, -18)` with identity basis. Then **+X renders to the right** (non-mirrored) and +Y is up. This is the natural front/side view. - A camera at `+Z` with identity basis looks **away** from the scene → you see only sky. (Common mistake.) - `m_fFovAngle` is **vertical FOV in radians** (≈`0.9` ≈ 51°), plus `m_fNearClip` / `m_fFarClip`. - Set `world.m_strPlayerCameraName` to the camera's `m_strEntityName` to render from it while Playing. A follow-cam is just a script on the camera that reads a target and calls `SetPosition` each frame (see §8). --- ## 6. Text sizing — `SETextEntity` (common blow-up) `SETextEntity` geometry is in **font pixel units** (glyph size **64 px**) pushed straight through the entity's world matrix, so it lands in **world units** at roughly `64 × scale`. A capital letter's normalized height is ~0.7, so **cap height ≈ 64 × 0.7 × `m_fScale` ≈ ~45 × `m_fScale` metres**. That makes the usable range **tiny** — `1.0` scale is a ~45 m letter; even `0.15` is ~6.7 m (screen-filling). To size a label, divide the wanted cap height (metres) by ~45: | Use | wanted cap height | `m_fScale` | |-----|-------------------|-----------| | floating world label ~1.3 m tall (e.g. "GOAL") | ~1.3 m | `~0.03` | | ~1 m tall caps | ~1 m | `~0.022` | | small in-world HUD / caption line | ~0.7 m | `~0.015 – 0.018` | (The reflected range floor is `0.01` ≈ a ~0.45 m cap — the smallest world text you can get with the default 64 px face. This is much smaller scale than screen-space HUD text, which shares the same font at `~1–2` because there 1 unit ≈ 1 pixel; in world space 1 unit ≈ 1 metre.) - `m_bBillboarded: "True"` turns the text to face the camera. - `m_HorizontalAlign` / `m_VerticalAlign` (`Left/Center/Right`, `Top/Middle/Bottom`) anchor the block around the entity origin. - Keep strings short in-world; long lines get wide fast (width scales with the same factor). - Text only draws in the **forward pass** — fine for the default pipeline. --- ## 7. Procedural geometry — `SEProceduralMeshEntity` The workhorse for authored levels: **many primitives baked into one mesh**, no model files. Geometry is entirely in `m_aParts` (array of `SEProceduralShapePart`); material is entity-level. ```json { "type": "SEProceduralMeshEntity", "name": "goal", "members": { "m_Transform": "{1,0,0,0}{0,1,0,0}{0,0,1,0}{ 30 , 4 , 0 , 1 }", "m_strEntityName": "goal", "m_bFrustumCullable": "True", "m_strScriptFile": "", "m_strScriptClass": "", "m_bScriptEnabled": "True", "m_ModelName": "", "m_pPhysicsModel": { "$ref": null }, "m_aParts": [ { "type": "SEProceduralShapePart", "members": { "m_ShapeType": "Box", "m_Size": "{ 6 , 1 , 4 }", "m_Offset": "{ 0 , -0.5 , 0 }", "m_RotationAxis": "{ 0 , 1 , 0 }", "m_RotationDegrees": "0", "m_Color": "{ 1 , 1 , 1 }", "m_uiRadialSegments": "24", "m_uiRings": "16", "m_UVScale": "{ 1 , 1 }", "m_UVOffset": "{ 0 , 0 }", "m_bTileWorld": "False", "m_Outline": "" } } ], "m_AlbedoTexture": "", "m_NormalTexture": "", "m_MetallicRoughnessTexture": "", "m_BaseColor": "{ 1 , 0.82 , 0.28 }", "m_Metallic": "0.9", "m_Roughness": "0.25", "m_bDoubleSided": "False", "m_bAlphaTest": "False" } } ``` Key facts (see `ai_schema.md` + `RemoteControl_AgentGuide.md` §3 for the full per-part detail): - `m_ShapeType`: `Box | Plane | Sphere | Cylinder | Cone | Torus | Extrude | Polygon`. - `m_Size` = extents the shape fits into; `m_Offset` = local placement inside the mesh. - **Colour:** final albedo = entity `m_BaseColor` × albedo texture × per-part `m_Color`. **If a mesh looks white, `m_BaseColor` was left at the default `{1,1,1}` — set it.** Keep parts white to show `m_BaseColor` / a texture pure. - `Extrude` / `Polygon` take a footprint from `m_Outline` (`"x0,z0 x1,z1 ..."`, metres). - Textures are **entity-level** (one material per mesh), cooked `.dds`; MR map is glTF-packed (R=AO, G=rough, B=metal). --- ## 8. Scripted gameplay (AngelScript) Attach a behavior with `m_strScriptClass` + inline `m_strScriptSource` (a JSON string — a scene file line has no newlines, so write the class on one line and escape inner `"` as `\"`). `m_strScriptFile` (path to an `.as`) is the alternative. `m_bScriptEnabled: "True"` to tick. ```angelscript class Behavior { SESceneEntity@ owner; Behavior(SESceneEntity@ o) { @owner = o; } // required ctor: takes the owning entity void OnInit() { } // once before first tick — resolve names here, not per frame void OnUpdate(float dt){} // every frame while Playing void OnDestroy() { } // on removal } ``` - Every reflected member is a **direct property** on the handle (`owner.m_Text = "Score";`); every `// script API` line in the schema is a **callable method** (`owner.SetPosition(p)`). - Globals: **`gWorld`** (`FindEntityByName`, `GetEntitiesCount`, `GetEntity`, `AddEntity`) and **`gInput`** (`IsLeftDown/IsRightDown/IsUpDown/IsDownDown/IsSpaceDown`, `IsKeyDown(int)`). - `SEVec3` value type: `.x/.y/.z`, `SEVec3(x,y,z)`, `+ - *float`, `Length()/Normalize()`. - **Scripts only tick while Playing.** They **cannot spawn/delete entities or touch files/network** — "remove" a thing by scaling it to zero and moving it away, or recycle it. Cast to reach a derived API: `cast(gWorld.FindEntityByName("cam"))`. Follow-camera (put this on the camera entity; identity transform, position on −Z): ```angelscript class FollowCam { SESceneEntity@ owner; SESceneEntity@ target; FollowCam(SESceneEntity@ o){ @owner=o; } void OnInit(){ @target = gWorld.FindEntityByName("player"); } void OnUpdate(float dt){ if (target is null) return; SEVec3 t = target.GetPosition(); owner.SetPosition(SEVec3(t.x, t.y + 1.5f, -18.0f)); } } ``` --- ## 9. Minimal skeleton to start from ```json { "world": { "type": "SESceneWorld", "members": { "m_strPlayerCameraName": "cam" } }, "entities": [ { "type": "SESkyBoxEntity", "name": "Sky", "members": { "m_Transform": "{1,0,0,0}{0,1,0,0}{0,0,1,0}{0,0,0,1}", "m_bFrustumCullable": "False", "m_strEntityName": "Sky", "m_strScriptClass": "", "m_bScriptEnabled": "True", "m_SkyTextureName": "textures/sky/skybox_puresky.dds", "m_SunColor": "{ 1 , 0.92 , 0.76 }", "m_SunAngles": "{ 130 , 50 }", "m_SunShininessStrength": "{ 256 , 8 }" } }, { "type": "SELightEntity", "name": "Sun", "members": { "m_Transform": "{1,0,0,0}{0,1,0,0}{0,0,1,0}{0,0,0,1}", "m_bFrustumCullable": "False", "m_strEntityName": "Sun", "m_strScriptClass": "", "m_bScriptEnabled": "True", "m_LightType": "Directional", "m_Color": "{ 1 , 0.97 , 0.9 }", "m_Intensity": "3.0", "m_AnglesDegrees": "{ 40 , -55 }", "m_bCastsShadow": "True" } }, { "type": "SECameraSceneEntity", "name": "cam", "members": { "m_Transform": "{1,0,0,0}{0,1,0,0}{0,0,1,0}{ 0 , 2.5 , -18 , 1 }", "m_bFrustumCullable": "False", "m_strEntityName": "cam", "m_strScriptClass": "", "m_bScriptEnabled": "True", "m_fFovAngle": "0.9", "m_fNearClip": "0.1", "m_fFarClip": "300" } } ] } ``` A complete, scripted, playable example is `Runtime/platformer_scene.json` (side-scroller: procedural platforms, a player behavior with gravity/jump/landing, a **horizontally moving platform** the player rides — the player script reads the mover's live position each frame and carries the player with it, a follow-cam, a gold goal, billboarded labels, and a **win condition** — reaching the goal flips the HUD text to "YOU WIN!" by writing another entity's `m_Text` property via `cast`). Load it and press Play. It's a good template for the patterns above: cross-entity script references, riding moving geometry, and HUD updates from script. --- ## 10. Checklist / common mistakes - [ ] **Booleans are `"True"`/`"False"`** (this format), not `true`. Include a Sky + a directional light or the scene renders black/unlit. - [ ] **Every value is a quoted string** — including numbers, bools, vectors, colors. - [ ] **Placement is `m_Transform`**, translation in the 4th row. No `position` key here. - [ ] **Set `m_strEntityName`** on anything a script or the player camera references. - [ ] **Camera on the −Z side looking +Z** (identity basis), or you'll see only sky. FOV is radians. - [ ] **Text `m_fScale` ~0.02–0.03** for world labels (cap height ≈ `45 × scale` metres), not ~0.1+. - [ ] **Set `m_BaseColor`** on procedural meshes or they render white. - [ ] Set `world.m_strPlayerCameraName` and add a `SECameraSceneEntity` if you want a play view. - [ ] Escape inline script `"` as `\"`; keep the class on one line (scene JSON has no newlines). - [ ] Validate the JSON parses before loading. - [ ] Texture/model/skybox paths are relative to the working dir (`Runtime`) and must be cooked. ## 11. Related docs - `ai_schema.md` — authoritative, build-matched list of every type, member, enum, and script API. - `RemoteControl_AgentGuide.md` — the live command bridge for mutating a *running* scene. - `Engine/SETechAI/SETechAI_Design.md` — architecture behind both.