create-playcanvas 0.8.1 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/package.json +2 -2
  2. package/skills/add-effects/SKILL.md +21 -83
  3. package/skills/apply-conventions/SKILL.md +33 -59
  4. package/skills/assemble-scene/SKILL.md +18 -43
  5. package/skills/assemble-scene/references/direct-engine.md +2 -2
  6. package/skills/assemble-scene/references/react.md +3 -3
  7. package/skills/assemble-scene/references/web-components.md +2 -2
  8. package/skills/bake-lighting/SKILL.md +25 -56
  9. package/skills/build-app/SKILL.md +38 -37
  10. package/skills/build-app/references/web-components.md +3 -3
  11. package/skills/calibrate-model/SKILL.md +18 -48
  12. package/skills/calibrate-model/references/web-components.md +3 -3
  13. package/skills/configure-animation/SKILL.md +21 -37
  14. package/skills/configure-animation/references/direct-engine.md +2 -2
  15. package/skills/configure-animation/references/react.md +3 -3
  16. package/skills/find-examples/SKILL.md +24 -60
  17. package/skills/inspect-glb/SKILL.md +21 -62
  18. package/skills/inspect-runtime/SKILL.md +41 -0
  19. package/skills/inspect-runtime/scripts/devtools-hook.js +23 -0
  20. package/skills/light-scene/SKILL.md +29 -97
  21. package/skills/load-assets/SKILL.md +52 -0
  22. package/skills/override-shader-chunks/SKILL.md +21 -61
  23. package/skills/reduce-draw-calls/SKILL.md +24 -54
  24. package/skills/reuse-scripts/SKILL.md +23 -46
  25. package/skills/reuse-scripts/references/direct-engine.md +3 -3
  26. package/skills/verify-pixels/SKILL.md +24 -58
  27. package/templates/engine/base/README.md +4 -0
  28. package/templates/engine/base/package.json +3 -2
  29. package/templates/engine/base/src/inspector.css +33 -0
  30. package/templates/engine/base/src/inspector.ts +38 -0
  31. package/templates/engine/base/tsconfig.json +1 -1
  32. package/templates/engine/base/vite.config.ts +16 -0
  33. package/templates/engine/interactive-sphere/src/App.ts +2 -2
  34. package/templates/engine/model-viewer/src/main.ts +1 -2
  35. package/templates/engine/product-configurator/src/main.ts +6 -7
  36. package/templates/react/base/README.md +4 -0
  37. package/templates/react/base/package.json +3 -2
  38. package/templates/react/base/src/inspector.css +33 -0
  39. package/templates/react/base/src/inspector.ts +38 -0
  40. package/templates/react/base/vite.config.ts +12 -1
  41. package/templates/react/product-configurator/src/App.tsx +6 -6
  42. package/templates/web-components/base/README.md +4 -0
  43. package/templates/web-components/base/package.json +4 -3
  44. package/templates/web-components/base/src/inspector.css +33 -0
  45. package/templates/web-components/base/src/inspector.ts +38 -0
  46. package/templates/web-components/base/tsconfig.json +1 -1
  47. package/templates/web-components/base/vite.config.ts +16 -0
  48. package/templates/web-components/interactive-sphere/src/main.ts +1 -2
  49. package/templates/web-components/product-configurator/src/main.ts +5 -6
  50. package/skills/build-hud/SKILL.md +0 -38
  51. package/skills/manage-game-state/SKILL.md +0 -48
@@ -1,109 +1,41 @@
1
1
  ---
2
2
  name: light-scene
3
- description: Use when a PlayCanvas scene must read as a deliberate image rather than a flat asset preview — photoreal and cinematic, or stylized, low-poly, and palette-driven — to set key and ambient lighting, sky and image-based lighting, shadows, tone mapping and exposure, water, and a restrained post-process grade of bloom, fog, and colour.
3
+ description: Use when lighting or rendering a PlayCanvas scene, covering lights, ambient and environment lighting, sky, shadows, tone mapping, exposure, fog, and post-processing.
4
4
  ---
5
5
 
6
- # Cinematic rendering
6
+ # Light and render a scene
7
7
 
8
- A polished frame is lit and graded, not just populated. Treat lighting, sky, shadows, tone mapping,
9
- and post as one pass over the assembled scene, and tune every value against a real screenshot rather
10
- than from memory.
8
+ Engine defaults give a flat, dark image; change them deliberately. Tune against inspected
9
+ screenshots, one class of value at a time.
11
10
 
12
- ## Confirm art direction before implementation
11
+ ## Defaults to change
13
12
 
14
- Follow the user's specified or approved direction. When it is unresolved, present concrete options
15
- using references, mockups, or inexpensive rendered variants and get approval before substantial
16
- implementation. Prefer a side-by-side variant sheet when cheap to produce. Work within the approved
17
- direction and confirm significant departures before implementing them.
13
+ - `scene.ambientLight` is black, so unlit sides render pure black. Light ambient and reflections
14
+ from an environment atlas or skybox, or set an ambient colour.
15
+ - Cameras default to `TONEMAP_LINEAR`. Choose `TONEMAP_NEUTRAL`, `TONEMAP_ACES2`, or
16
+ `TONEMAP_FILMIC` (on `cameraFrame.rendering.toneMapping` when using `CameraFrame`), set exposure,
17
+ then balance light intensities to it.
18
+ - Shadows are off. Enable `castShadows` on the key light. Directional `shadowDistance` is 40 m, so
19
+ use cascades for large scenes, and `SHADOWUPDATE_THISFRAME` for static shadows. Size resolution to
20
+ the framed subject.
21
+ - Lights shine along local -Y (`apply-conventions`).
18
22
 
19
- ## Choose the path from the brief
23
+ ## Use what ships
20
24
 
21
- Read the reference before wiring any pipeline. A photoreal or cinematic brief takes the runtime HDR
22
- path below. A brief whose reference is low-poly, flat-shaded, toon, or palette-driven — or whose
23
- startup, frame, or payload budget rules out the runtime pipeline — takes the stylized path: banded
24
- or flat shading through `override-shader-chunks`, a solid or gradient sky, static lighting through
25
- `bake-lighting`, and no `Water` or `CameraFrame`. A remark such as "look at the water script" is a
26
- pointer to inspect it; confirm any resulting change of art direction before integration. On either
27
- path, dynamic objects and their attachments must sample or approximate the baked or ambient occlusion
28
- so they dim to the same levels as the surfaces around them, grounded with contact shadows.
25
+ - `CameraFrame` handles tone mapping, bloom, SSAO, DOF, vignette, and grading. Call `update()` after
26
+ changes and destroy it with the app. Keep bloom intensity within the documented 0 to 0.1.
27
+ - `scene.sky.type` (`SKYTYPE_DOME`, `SKYTYPE_BOX`, `SKYTYPE_INFINITE`) grounds an environment sky.
28
+ - The `ProceduralSky` script owns a linked sun light: set the light's base intensity, then tune
29
+ elevation and azimuth on the sky, not the light.
30
+ - The `ShadowCatcher` script grounds subjects that have no lit floor.
31
+ - Clustered lighting is on by default, so many point and spot lights are cheap.
32
+ - Bake static lights with `bake-lighting`; stylize shading with `override-shader-chunks`.
29
33
 
30
- ## Runtime HDR path
34
+ Find each integration's example with `find-examples`.
31
35
 
32
- Prefer shipped building blocks over hand-written shaders and passes. Discover them with the
33
- `reuse-scripts` skill and adapt the closest official example with `find-examples`:
36
+ ## Warning signs
34
37
 
35
- - the core `CameraFrame` helper for tone mapping, ambient occlusion, bloom, vignette, and grade;
36
- - a procedural sky and image-based light for ambient colour and reflections;
37
- - a shadow catcher for grounding subjects that have no lit floor.
38
-
39
- For an outdoor scene whose brief asks for reflective, photoreal water, stop before authoring the
40
- scene and read both `reuse-scripts` and `find-examples`. Use `graphics/water.example.mjs` as the
41
- initial composition recipe: it already joins the production `Water` script, sky, sun, `CameraFrame`,
42
- water layer, scene depth, and reflection pipeline. Get that complete frame rendering before
43
- replacing its assets or tuning its look; a custom normal-mapped material or tessellated plane is not
44
- that baseline.
45
-
46
- For that water scene, keep both production integrations visible in the implementation:
47
-
48
- - import and create `Water` from `playcanvas/scripts/esm/water.mjs` on the water entity, preserving
49
- the example's required render component, camera reference, water layer, depth map, and textures;
50
- - construct `CameraFrame` from `playcanvas` for the gameplay camera, set a deliberate tone map and
51
- restrained grade, call `update()`, and destroy it with the application.
52
-
53
- Do not continue to cosmetic tuning while a required integration is missing or diagnostics report
54
- a shader, texture, or framebuffer error.
55
-
56
- ## Light and expose
57
-
58
- - Establish one dominant key light with a clear direction and warm or cool intent, then add soft
59
- fill or ambient so shadows are not crushed to black. A single flat ambient with no key reads as an
60
- asset gallery.
61
- - Choose a physically based tone mapping and a deliberate exposure, then balance light intensities to
62
- that exposure, not the reverse.
63
- - Drive ambient and reflections from the sky or an environment map, not a constant colour, so
64
- materials pick up their surroundings.
65
- - Tune the HDR stack in isolation before styling it. Begin with neutral tone mapping, scene exposure
66
- `1`, procedural-sky luminance near its official-example value, bloom off, grading neutral, fog off,
67
- and reflection/specular strengths at their defaults. Capture a screenshot, then change one class of
68
- value at a time. Do not simultaneously raise key intensity, sky luminance, exposure, reflection,
69
- saturation, fog brightness, and bloom.
70
- - Treat linked procedural skies as the owner of the sun light's direction, colour, and intensity.
71
- Set the light's base intensity before assigning it to the sky and tune the sky elevation/azimuth;
72
- do not also hand-author the light transform or colour as though they remained independent.
73
- - Preserve the source assets' albedo colours. If diffuse texture colours collapse toward white or a
74
- single warm tint, reduce the HDR stack before compensating with darker material colours.
75
-
76
- ## Shadow and ground
77
-
78
- - Enable shadows on the key light and size the shadow area and resolution to the framed subject, not
79
- the whole world. Soften the penumbra rather than only raising resolution.
80
- - Ground subjects with a contact or catcher shadow when the surface beneath them is not itself lit.
81
-
82
- ## Grade with restraint
83
-
84
- - Bloom, fog, vignette, and colour grade unify a frame; overdone bloom and heavy fog destroy
85
- silhouette and depth. Start subtle and increase only against the screenshot. `CameraFrame` bloom
86
- intensity is documented for the `0` to `0.1` range; stay inside it.
87
- - Match fog colour and density to the sky so the horizon stays coherent.
88
- - Preserve surface detail through the brightest reflection path. If glare clips a large part of the
89
- frame to white or a reflection becomes an opaque black blob, rebalance exposure, light intensity,
90
- reflection strength, and shadow bias before adding more post-processing.
91
- - Compare both the brightest surface and the shadow-facing side of the hero subject in the same
92
- screenshot. Reject the grade if either broad highlight clipping or crushed-black silhouette hides
93
- texture detail. A mechanically valid frame is not visually complete while either failure remains.
94
-
95
- ## Prove the look
96
-
97
- For changes to an existing look, show old and new at the same pose in one image. If the difference
98
- is not visible, report that before spending more time tuning.
99
-
100
- Tune against pixels, not numbers. Capture the intended framing with an image-returning browser
101
- screenshot tool and inspect the returned image, as `apply-conventions` and the local agent guide
102
- require; a saved filepath is not evidence. Use two fixed poses, capture with a headless browser, and
103
- return images at accept-or-reject points only, not after every edit. Confirm the subject is exposed
104
- and readable, shadows are present but not crushed, and the grade is restrained, from the real
105
- gameplay camera at the target resolution. Confirm the post-process camera keeps rendering as it
106
- orbits and pitches.
107
-
108
- Choose the authoring surface with the `build-app` skill and set lights, camera, and scene settings
109
- through its own primitives before reaching into the Engine.
38
+ - Albedo washing toward white: reduce exposure, sky, and light intensity before darkening
39
+ materials.
40
+ - Large areas clipped white or crushed black: rebalance exposure and lights before adding post.
41
+ - A fog colour that differs from the sky horizon breaks the horizon line.
@@ -0,0 +1,52 @@
1
+ ---
2
+ name: load-assets
3
+ description: Use when loading glTF/GLB models or textures in PlayCanvas, choosing Draco or Basis/KTX2 compression, or setting up decoders and preloading.
4
+ ---
5
+
6
+ # Load assets
7
+
8
+ ## glTF is fully supported
9
+
10
+ Load `.glb` files as `container` assets and call `asset.resource.instantiateRenderEntity()`, not
11
+ `instantiateModelEntity`. The importer already handles `KHR_lights_punctual`, `KHR_materials_*`
12
+ (clearcoat, sheen, transmission, volume, iridescence, variants, and more), `KHR_texture_transform`,
13
+ and `EXT_mesh_gpu_instancing`; do not rebuild these by hand. Preload asset sets with
14
+ `AssetListLoader`.
15
+
16
+ ## Compress for size
17
+
18
+ - Geometry: Draco (`KHR_draco_mesh_compression`). Meshopt is not supported, so decode such files
19
+ before shipping.
20
+ - Textures: KTX2 with Basis (`KHR_texture_basisu`) transcodes to a GPU format, cutting both
21
+ download size and VRAM. ETC1S is smallest and lossy. UASTC is larger and keeps
22
+ detail, which matters most for normal maps. Weigh size against visible detail.
23
+
24
+ ## Set up decoders
25
+
26
+ The npm package ships no Draco or Basis wasm. Copy `draco/` and `basis/` from the Engine's
27
+ `examples/assets/wasm` at the installed tag (`find-examples`), and configure them before loading:
28
+
29
+ ```js
30
+ import { WasmModule, basisInitialize } from 'playcanvas';
31
+
32
+ WasmModule.setConfig('DracoDecoderModule', {
33
+ glueUrl: '/wasm/draco/draco.wasm.js',
34
+ wasmUrl: '/wasm/draco/draco.wasm.wasm',
35
+ fallbackUrl: '/wasm/draco/draco.js'
36
+ });
37
+ await new Promise((resolve) => WasmModule.getInstance('DracoDecoderModule', resolve));
38
+ basisInitialize({
39
+ glueUrl: '/wasm/basis/basis.wasm.js',
40
+ wasmUrl: '/wasm/basis/basis.wasm.wasm',
41
+ fallbackUrl: '/wasm/basis/basis.js'
42
+ });
43
+ ```
44
+
45
+ In Web Components, a `<pc-wasm name="DracoDecoderModule">` element under `<pc-app>` sets the Draco
46
+ config with `glue`, `wasm`, and `fallback` attributes.
47
+
48
+ ## Other assets
49
+
50
+ - Raise `texture.anisotropy`, which defaults to 1, for surfaces viewed at grazing angles.
51
+ - Gaussian splats load as `gsplat` assets onto a `gsplat` component; start from a
52
+ `gaussian-splatting` example.
@@ -1,65 +1,25 @@
1
1
  ---
2
2
  name: override-shader-chunks
3
- description: Use when a PlayCanvas StandardMaterial needs custom shading its properties cannot express — procedural or stylized looks, custom vertex displacement, per-material screen effects — before writing a standalone shader from scratch.
3
+ description: Use when a PlayCanvas StandardMaterial needs custom shading its properties cannot express, such as stylized looks or vertex displacement, before writing a ShaderMaterial.
4
4
  ---
5
5
 
6
- # Override shading through chunks
7
-
8
- A `StandardMaterial`'s shader is assembled from named chunks (lighting, combine, transform, and
9
- more). Overriding one keeps the rest of the pipeline — fog, tonemapping, shadows, skinning,
10
- instancing — intact. Reach for a fully custom `ShaderMaterial` only when the look needs a stage the
11
- chunk system does not expose at all (its own vertex/fragment pair and attribute layout); a chunk
12
- override is the default for everything else, since it is cheaper and stays integrated.
13
-
14
- ## Pin the version, read the source
15
-
16
- Set `material.shaderChunksVersion` to the installed engine's major.minor (for example `'2.22'`)
17
- before shipping any override. Chunk names and their contracts — what a chunk defines, what globals
18
- it reads or writes — are version-keyed and change between releases. Read the installed `playcanvas`
19
- source for the exact chunk being touched; do not recall its contract from memory or an older
20
- version's documentation.
21
-
22
- ## Clone before overriding a shared material
23
-
24
- Clone a material that other mesh instances share — including default and asset-imported materials —
25
- before changing its chunks, properties, or material-level parameters for only some meshes, and assign
26
- the clone. For per-mesh uniforms, use `meshInstance.setParameter()` without cloning the material.
27
-
28
- ## Set chunks through the documented accessor
29
-
30
- Use `material.getShaderChunks(SHADERLANGUAGE_GLSL).set(name, source)`, and the WGSL equivalent —
31
- the public, documented method — not the underlying `shaderChunks` maps, which are internal. Call
32
- `material.update()` after any change so cached shader variants are cleared.
33
-
34
- ## Emissive-only overrides
35
-
36
- An `emissivePS` override implements `void getEmission()`, writing the result into `dEmission`. When
37
- the override is meant to fully drive the visible color, zero the material's `diffuse`/`specular` and
38
- disable `useSkybox` so no other lighting term still contributes underneath it.
39
-
40
- ## Uniforms are raw
41
-
42
- `setParameter` uploads exactly what it is given, with no color-space conversion. Convert any
43
- authored sRGB color with `Color#linear()` before calling `setParameter`; an sRGB color passed
44
- straight through renders visibly darker and less saturated than it was authored.
45
-
46
- ## Keep vertex chunks instancing-aware
47
-
48
- A custom `transformCoreVS` (or any vertex chunk reading the model matrix) must keep the engine's
49
- `INSTANCING` define path — pulling the per-instance matrix under that define — with a
50
- non-instanced fallback that uses `matrix_model` directly, matching the stock chunk's structure.
51
- Dropping this path silently breaks instancing for any mesh instance using the material.
52
-
53
- ## Provide GLSL and WGSL, or pin to WebGL
54
-
55
- Override both `SHADERLANGUAGE_GLSL` and `SHADERLANGUAGE_WGSL` chunks for anything touched. On a
56
- WebGPU device, a chunk supplied only in GLSL forces the material's entire shader — not just that
57
- chunk — onto a GLSL-compiled-then-transpiled path; without the optional transpiler modules loaded
58
- (the common case), that shader fails to compile. Supply both languages, or explicitly pin the
59
- graphics device to WebGL if WGSL support is not required.
60
-
61
- ## Expand bounds for vertex displacement
62
-
63
- If the override displaces vertices, expand the mesh's bounds by the maximum displacement (see
64
- `apply-conventions`) — CPU-computed bounds never observe shader-side deformation, so culling breaks
65
- silently otherwise.
6
+ # Override shader chunks
7
+
8
+ Overriding a `StandardMaterial` chunk keeps fog, tone mapping, shadows, skinning, and instancing
9
+ intact. Use a `ShaderMaterial` only when the look needs its own vertex and fragment pair.
10
+
11
+ - Set `material.shaderChunksVersion` to the installed major.minor, such as `'2.23'`. Chunk names
12
+ and contracts change between versions, so read the installed chunk source, not memory.
13
+ - Set chunks with `material.getShaderChunks(SHADERLANGUAGE_GLSL).set(name, source)` and the WGSL
14
+ equivalent, then call `material.update()`. For every material on a device, use
15
+ `ShaderChunks.get(device, language)`; the global `shaderChunks` object is deprecated.
16
+ - Supply both GLSL and WGSL. On WebGPU, one GLSL-only chunk sends the whole shader through a
17
+ transpiler that is usually not loaded, so it fails to compile. Otherwise pin the device to WebGL.
18
+ - Clone shared materials, including imported ones, before overriding them for only some meshes.
19
+ Per-mesh uniforms use `meshInstance.setParameter()`.
20
+ - Uniforms are raw; convert sRGB colours with `Color#linear()`.
21
+ - Vertex chunks that read the model matrix, such as `transformCoreVS`, must keep the `INSTANCING`
22
+ path with a `matrix_model` fallback, like the stock chunk.
23
+ - Expand mesh bounds by the maximum displacement; CPU bounds do not see shader deformation.
24
+ - For an emissive-only look, implement `getEmission()` writing `dEmission`, zero `diffuse` and
25
+ `specular`, and disable `useSkybox`.
@@ -1,67 +1,37 @@
1
1
  ---
2
2
  name: reduce-draw-calls
3
- description: Use when a PlayCanvas application submits too many draw calls or frame time is CPU-bound — repeated meshes, dense grids, always-on interface elements — before hand-writing mesh merging or custom renderers.
3
+ description: Use when a PlayCanvas app has too many draw calls or is CPU-bound, before hand-merging meshes or writing custom renderers.
4
4
  ---
5
5
 
6
- # Cut draw calls, ranked by cost
6
+ # Cut draw calls
7
7
 
8
- Work the ladder in order and stop at the first rung that clears the budget. Each rung trades away
9
- less flexibility than the next; skipping straight to a custom renderer or hand-written mesh merge
10
- costs more engineering time than the draw calls it saves.
8
+ Measure first with `app.stats.drawCallCount` or `MiniStats`; the per-pass `app.stats.drawCalls`
9
+ breakdown fills only in profiler builds. Also halve the pixel ratio and call `app.resizeCanvas()`:
10
+ if frame time drops, the cost is fill rate, not draw calls. Stop at the first rung that meets the
11
+ budget and report before-and-after counts.
11
12
 
12
- ## Measure first
13
+ ## 1. Stop drawing hidden things
13
14
 
14
- Read `app.stats.drawCalls.total` (or the `forward`/`depth`/`shadow` breakdown) before changing
15
- anything, or drop in `MiniStats` for a live overlay. Every rung below is proved against this number,
16
- not against intuition about what "looks expensive."
15
+ An element at opacity 0 still draws. Set `enabled = false`.
17
16
 
18
- Draw count alone does not identify the bottleneck. Test sensitivity to pixel cost by halving the
19
- effective pixel ratio (`Math.min(graphicsDevice.maxPixelRatio, window.devicePixelRatio)`) and calling
20
- `app.resizeCanvas()`. Verify the backbuffer shrank and compare frame times at the same scene state.
21
- If frame time improves materially, prioritize resolution, multisampling and per-pixel shader cost.
22
- Otherwise profile CPU and GPU work before choosing a rung; restore the original ratio after the test.
17
+ ## 2. BatchManager
23
18
 
24
- ## Rung 1: stop drawing invisible things
19
+ Call `app.batcher.addGroup(name, dynamic, maxAabbSize)`, then set `batchGroupId` on render, sprite,
20
+ or element components. Use `dynamic: false` for geometry that never moves. Members join when
21
+ enabled in the live tree, in any order, and groups regenerate automatically when membership
22
+ changes. Call `markGroupDirty(id)` only after moving members of a static group. Manual `AppOptions`
23
+ bootstraps must set `batchManager`.
25
24
 
26
- An element at opacity 0 still submits a draw call — its mesh instance exists and is still in a
27
- layer, so the GPU processes it every frame for no visible result. Toggle `enabled` on the entity to
28
- actually skip it. Check every element that is ever fully transparent, not only the ones on screen
29
- when you're profiling — this rung is easy to skip precisely because nothing looks wrong.
25
+ ## 3. Hardware instancing
30
26
 
31
- ## Rung 2: `BatchManager` — merge without touching shaders
27
+ Use this for many copies of one mesh and material with per-frame transforms. Build a buffer with
28
+ `VertexFormat.getDefaultInstancingFormat(device)`, one mat4 per instance, and call
29
+ `meshInstance.setInstancing(vb)`; `instancingCount` follows the buffer.
32
30
 
33
- `app.batcher.addGroup(name, dynamic, maxAabbSize)`, then set `batchGroupId` on each member's
34
- component (render, sprite, or UI element). Use `dynamic: false` for geometry that never moves.
31
+ There is no per-instance culling. By default the group always draws; `setInstancing(vb, true)`
32
+ culls it as one unit against a `RenderComponent#customAabb` covering every instance. Split large
33
+ instance sets into spatial chunks. Custom vertex chunks need the `INSTANCING` path
34
+ (`override-shader-chunks`). Start from the `graphics/instancing-basic` and
35
+ `graphics/instancing-custom` examples.
35
36
 
36
- Contract: the `batchGroupId` setter only inserts into the batcher while `entity.enabled` is true,
37
- and that flag requires both the local enable state and hierarchy attachment — set `batchGroupId`
38
- after the entity is parented into the live tree, not before, or the member silently drops out of the
39
- group. `BatchManager.generate()` runs once automatically on the app's first rendered frame; if group
40
- membership changes afterward, call `app.batcher.markGroupDirty(id)` (or `generate([id])`) yourself.
41
- An Engine-only app without the full `Application` bootstrap must register the class via
42
- `AppOptions.batchManager` before it can batch anything.
43
-
44
- ## Rung 3: hardware instancing — merge without touching layout
45
-
46
- Reach for this once distinct materials or per-frame transform updates would defeat `BatchManager`.
47
- Build a per-instance vertex buffer with `VertexFormat.getDefaultInstancingFormat(device)` (one mat4
48
- per instance), call `meshInstance.setInstancing(vb)`, and set `instancingCount`. A custom vertex
49
- chunk needs its own `INSTANCING` code path with an identity-matrix fallback for the non-instanced
50
- case (cross-reference `override-shader-chunks`).
51
-
52
- Culling trade-off: instanced meshes cull as one unit against a single bounding volume — an
53
- off-screen instance inside an otherwise-visible group still draws unless you opt in. Pass
54
- `setInstancing(vb, true)` and set a `RenderComponent#customAabb` spanning every instance's world
55
- extent so the renderer has something correct to cull against instead of culling nothing or
56
- culling the whole group by one instance's bounds.
57
-
58
- Adapt the official recipes rather than deriving the buffer layout or vertex-shader wiring from
59
- memory: `graphics/instancing-basic` for the format/buffer contract, `graphics/instancing-custom` for
60
- the vertex-shader side. Locate both at the installed engine version via `find-examples`.
61
-
62
- ## Prove and report
63
-
64
- Verify the rendered image is unchanged with `verify-pixels` before calling the change done —
65
- merging or instancing must not move a single pixel. Re-measure `app.stats.drawCalls` afterward and
66
- state the before/after counts in the change description; "fewer draw calls" without numbers is not
67
- a result.
37
+ Confirm the image is unchanged with `verify-pixels`.
@@ -1,12 +1,13 @@
1
1
  ---
2
2
  name: reuse-scripts
3
- description: Use before implementing non-core PlayCanvas behavior to discover the installed Engine's curated scripts and reuse or adapt those that fit the required behavior and approved art direction.
3
+ description: Use before writing PlayCanvas behaviour from scratch, to reuse the scripts and helpers the installed Engine already ships.
4
4
  ---
5
5
 
6
- # Engine scripts
6
+ # Reuse shipped Engine code
7
7
 
8
- Before writing behavior from scratch, inspect the curated scripts shipped with the installed
9
- `playcanvas` package under `scripts/esm/**`. Discover the current set:
8
+ Before writing behaviour, check what the installed `playcanvas` package already provides.
9
+
10
+ Scripts live under `scripts/esm/**`; import only from there. List them:
10
11
 
11
12
  ```sh
12
13
  rg 'static scriptName =' node_modules/playcanvas/scripts/esm \
@@ -14,50 +15,26 @@ rg 'static scriptName =' node_modules/playcanvas/scripts/esm \
14
15
  | sort
15
16
  ```
16
17
 
17
- Read the selected file for its named export, `@attribute` properties, and defaults. Not every module
18
- is a `Script`; the parsers below `scripts/esm/parsers` are plain classes registered with a resource
19
- handler instead.
20
-
21
- Only import from `scripts/esm/**`. Legacy sibling directories depend on the global Engine namespace.
22
- After selecting a script, use `find-examples` to locate its matching versioned Engine example. When
23
- `node_modules/playcanvas` is a linked or source checkout, its `examples/src/examples/**` are already
24
- on disk; read them there and skip the fetch.
25
-
26
- ## Check fit before integration
27
-
28
- Compare candidates with the required behavior, approved art direction, and runtime constraints.
29
- Reuse a suitable script directly or adapt its configuration and extension points. If none fits,
30
- state the limitation and implement only the missing behavior, preserving reusable parts and their
31
- lifecycle, bounds, and input invariants.
32
-
33
- For visual behavior, resolve an unclear art direction with the user before substantial implementation.
34
- Present references, mockups, or inexpensive variants; follow existing approval and confirm significant
35
- departures. A script's default appearance is only a starting point for assessing its fit.
36
-
37
- ## Adapt the reference integration
38
-
39
- Treat the selected script source and its closest official example as complementary references:
18
+ Common ones are `CameraControls`, `FirstPersonController`, `ThirdPersonController`,
19
+ `ShadowCatcher`, `ProceduralSky`, `Water`, and `Grid`, plus XR and splat scripts. Modules under
20
+ `scripts/esm/parsers` are resource-handler classes, not scripts.
40
21
 
41
- 1. Read the source for exports, properties, defaults, fallbacks, required components, and lifecycle.
42
- 2. Read the example for assets, entity references, mesh requirements, layer ordering, scene settings,
43
- and render-pipeline setup.
44
- 3. Integrate the selected behavior with its required components, assets, and passes. Adapt the look
45
- to the approved direction and preserve defaults and dependencies that still apply.
46
- 4. After a rendered frame, fail on console, shader, or missing-asset diagnostics. Exercise the
47
- behavior with real input where applicable and inspect returned screenshots from representative
48
- views at the final backbuffer density.
49
- 5. Use the example to check integration correctness and the user's approved direction to judge the
50
- final look. Report any remaining gap.
22
+ Core exports that are not scripts include `CameraFrame` for post-processing, `Picker`
23
+ (`getSelectionAsync`), `TransformGizmo`, `OutlineRenderer`, `MiniStats`, `GltfExporter`, and
24
+ `UsdzExporter`.
51
25
 
52
- If no matching example exists, state that and derive the integration from installed source instead
53
- of inventing it from memory.
26
+ ## Integrate
54
27
 
55
- ## Preserve grouped defaults
28
+ 1. Read the source for exports, `@attribute` properties, defaults, and required components.
29
+ 2. Find its official example with `find-examples`, and copy the assets, layers, scene settings, and
30
+ render setup it depends on.
31
+ 3. Adapt configuration and extension points. If nothing fits, implement only the missing part and
32
+ say what was missing.
33
+ 4. After a rendered frame, fail on console, shader, or missing-asset errors.
56
34
 
57
- Grouped property updates differ by authoring surface. Read the selected reference and preserve
58
- defaults that are not being changed.
35
+ Keep a shipped script's input mapping and damping unless asked otherwise, and verify input
36
+ directions with real input rather than deriving signs from memory.
59
37
 
60
- Read exactly one reference matching the code being edited:
61
- [direct Engine](references/direct-engine.md), [React](references/react.md), or
62
- [Web Components](references/web-components.md). Choose from imports and markup, not installed
63
- dependencies alone.
38
+ Grouped property updates differ by surface; preserve defaults you do not change. Read only the
39
+ reference matching the code: [direct Engine](references/direct-engine.md),
40
+ [React](references/react.md), or [Web Components](references/web-components.md).
@@ -6,9 +6,9 @@ and attach it with `entity.script.create()`. Preserve an existing script compone
6
6
  Some production helpers are core Engine exports rather than scripts. Follow the matching example's
7
7
  construction and teardown contract instead of forcing them through `script.create`.
8
8
 
9
- Keep script constructors at module scope. `create` takes two distinct option keys that are not
10
- interchangeable: `properties` is assigned straight onto the instance, while `attributes` supplies
11
- declared `@attribute` fields. Passing the wrong one silently does nothing.
9
+ Keep script constructors at module scope. `create` takes two option keys: `attributes` is passed to
10
+ the constructor for declared `@attribute` fields, while `properties` is `Object.assign`ed onto the
11
+ instance after construction, overwriting any field it names.
12
12
 
13
13
  Prefer mutating an existing grouped property when only a few fields change; preserve other defaults
14
14
  and follow any update or rebuild contract in the selected source.
@@ -1,70 +1,36 @@
1
1
  ---
2
2
  name: verify-pixels
3
- description: Use when changing PlayCanvas rendering code that must not visibly change the rendered image — draw-call optimization, hardware instancing, shader or material refactors, noise or lightmap bakes, or asset pipeline swaps — before shipping.
3
+ description: Use when a PlayCanvas rendering change is meant to leave the image unchanged, to prove it with deterministic captures and pixel comparison.
4
4
  ---
5
5
 
6
6
  # Prove pixels unchanged
7
7
 
8
- Choose the gate before building the capture harness. Both classes require controlled captures
9
- that cover the affected surfaces.
8
+ ## Choose the gate
10
9
 
11
- ## Classify the change
10
+ - Byte-exact: the change keeps the same decoded data and the same shading math, so every pixel
11
+ must match.
12
+ - Side-by-side review: the change alters sampled values, random order, precision, or where math
13
+ runs. Report it as "not byte-gateable" and show old and new at each pose; do not invent
14
+ thresholds.
12
15
 
13
- | Change | Gate |
14
- | --- | --- |
15
- | Batching, instancing, mesh indexing, chunking, pooling that preserves RNG order, an asset format swap with identical decoded data | Byte-exact matrix |
16
- | Re-rolled or re-sampled procedural noise, a changed RNG consumption order, math moved between CPU and GPU, changed precision, fewer octaves or samples | Not byte-gateable: side-by-side review |
16
+ ## Make frames deterministic
17
17
 
18
- For the second class, sampled values or precision can change. Report it as "not byte-gateable" and
19
- use the side-by-side gate after the shared capture setup below; do not invent uncalibrated perceptual
20
- thresholds to approve it.
18
+ - Load every asset and match camera, canvas size, lighting, and animation time between builds. Seed
19
+ incidental randomness.
20
+ - Drive animated shaders from one app-owned time value, not `Date.now()` or `performance.now()`.
21
+ - `app.timeScale = 0` makes `dt` zero, but `update` still runs; anything counting frames or reading
22
+ wall-clock time still advances.
23
+ - Set `app.autoRender = false`, then `app.renderNextFrame = true` before each frame to capture.
21
24
 
22
- ## Cover every touched surface
25
+ ## Capture and compare
23
26
 
24
- List every touched material or surface and a capture pose it dominates. Use at least two
25
- representative poses, adding as many as coverage requires; a horizon strip does not cover a water
26
- shader change. For animated surfaces, include a second phase mid-animation.
27
+ - WebGL: `await (device as WebglGraphicsDevice).readPixelsAsync(x, y, w, h, pixels)`. WebGPU:
28
+ render into a `RenderTarget` and read its `colorBuffer` with `Texture#read()`. Avoid colour
29
+ conversion and lossy encoding.
30
+ - Capture the unmodified build twice first. The captures must match exactly, or the harness itself
31
+ is noisy.
32
+ - Cover every touched surface in at least two poses, plus a mid-animation phase where relevant.
33
+ - Report the count, for example "0 of 65536 pixels differ", or the count and where the differences
34
+ are. "Looks the same" is not a result.
27
35
 
28
- ## Make the frame deterministic
29
-
30
- For both gates, load assets fully and match the scene state, camera, canvas size, lighting, and
31
- animation time between builds. Seed incidental randomness so only the intended change varies.
32
-
33
- - Drive every animated shader or vertex effect from one app-owned time value, never `Date.now()` or
34
- `performance.now()` read inside the render path, so a captured phase is exactly reproducible.
35
- - Freeze the clock with `app.timeScale = 0` before capturing; nothing should advance between frames
36
- you did not explicitly step.
37
- - Step frames explicitly: set `app.autoRender = false` once, then set `app.renderNextFrame = true`
38
- before each frame you want rendered. The engine renders exactly that frame and clears the flag —
39
- do not rely on the free-running render loop plus a timed screenshot.
40
-
41
- ## Gate and report the byte-exact class
42
-
43
- Read the exact backbuffer with `await device.readPixelsAsync(x, y, w, h, pixels)`. In the installed
44
- engine this method lives on `WebglGraphicsDevice`, so narrow to it; WebGPU needs an equivalent
45
- readback. An existing capture path must preserve raw pixels without colour conversion or lossy
46
- encoding before comparison.
47
-
48
- Before trusting any diff between the old and new build, capture the same pose × phase matrix twice
49
- from the *unmodified* build. Two captures of identical, frozen state must be bit-identical. If they
50
- are not, the capture path itself is the source of noise — an unseeded animation, an asset still
51
- loading, a GPU timing race — and must be fixed before it can say anything about the real change.
52
-
53
- Byte-compare each pose × phase pair between the two builds; do not diff by looking. Zero differing
54
- pixels passes outright. Any nonzero diff must be reviewed on-screen, and its cause and extent stated
55
- in the change description — never merged silently. Report the actual count every time, for example
56
- "0 of 65536 pixels differ" or "312 of 65536 pixels differ, confined to the object's silhouette edge".
57
- "Looks the same" or "no visible difference" is not a result.
58
-
59
- ## Review the other class side by side
60
-
61
- For each planned pose and phase, show the controlled old and new captures together in one image
62
- for the user's accept or reject. State what differs and why, for example "noise tile replaces
63
- runtime fbm: pattern period changed, tint and amplitude match". If no difference is visible, report
64
- that. Skip the cross-build byte comparison for this class.
65
-
66
- ## Keep the harness small
67
-
68
- Reuse the project's capture harness when available. Keep harness code out of the shipped bundle,
69
- behind a dev-only import or in a tools directory, and list its files in the change description.
70
- Capture with a headless browser and return images only at accept-or-reject points, not after every edit.
36
+ Keep harness code out of the shipped bundle; attach it through `inspect-runtime`.
@@ -17,6 +17,10 @@ npm run dev
17
17
 
18
18
  Open <http://localhost:5173>. Edit the files under `src/` and save to see the scene update.
19
19
 
20
+ ## Inspector
21
+
22
+ `npm run dev` loads the [PlayCanvas Inspector](https://github.com/playcanvas/inspector), a debug panel for the hierarchy, assets, materials, textures, frame graph and physics. Press the backquote key (<kbd>&#96;</kbd>) to show or hide it, <kbd>F9</kbd> to pause and <kbd>F10</kbd> to step a frame. It is wired up in `vite.config.ts` and `src/inspector.ts` and is left out of production builds.
23
+
20
24
  ## Scripts
21
25
 
22
26
  | Command | Description |
@@ -15,14 +15,15 @@
15
15
  "typecheck": "tsc --noEmit"
16
16
  },
17
17
  "dependencies": {
18
- "playcanvas": "^2.22.6"
18
+ "playcanvas": "^2.23.0"
19
19
  },
20
20
  "devDependencies": {
21
21
  "@playcanvas/eslint-config": "3.0.0-beta.8",
22
+ "@playcanvas/inspector": "^0.2.0",
22
23
  "eslint": "^9.39.5",
23
24
  "prettier": "^3.9.9",
24
25
  "typescript": "~6.0.3",
25
- "vite": "^8.3.1"
26
+ "vite": "^8.3.2"
26
27
  },
27
28
  "engines": {
28
29
  "node": ">=22.23.2"