create-playcanvas 0.8.0 → 0.9.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 (36) hide show
  1. package/dist/index.mjs +35 -33
  2. package/package.json +4 -4
  3. package/skills/add-effects/SKILL.md +21 -72
  4. package/skills/apply-conventions/SKILL.md +33 -59
  5. package/skills/assemble-scene/SKILL.md +18 -43
  6. package/skills/assemble-scene/references/direct-engine.md +2 -2
  7. package/skills/assemble-scene/references/react.md +3 -3
  8. package/skills/assemble-scene/references/web-components.md +2 -2
  9. package/skills/bake-lighting/SKILL.md +25 -50
  10. package/skills/build-app/SKILL.md +29 -28
  11. package/skills/build-app/references/web-components.md +3 -3
  12. package/skills/calibrate-model/SKILL.md +18 -48
  13. package/skills/calibrate-model/references/web-components.md +3 -3
  14. package/skills/configure-animation/SKILL.md +21 -37
  15. package/skills/configure-animation/references/direct-engine.md +2 -2
  16. package/skills/configure-animation/references/react.md +3 -3
  17. package/skills/find-examples/SKILL.md +24 -60
  18. package/skills/inspect-glb/SKILL.md +21 -62
  19. package/skills/light-scene/SKILL.md +29 -82
  20. package/skills/load-assets/SKILL.md +52 -0
  21. package/skills/override-shader-chunks/SKILL.md +21 -55
  22. package/skills/reduce-draw-calls/SKILL.md +24 -48
  23. package/skills/reuse-scripts/SKILL.md +23 -42
  24. package/skills/reuse-scripts/references/direct-engine.md +8 -28
  25. package/skills/reuse-scripts/references/react.md +2 -12
  26. package/skills/reuse-scripts/references/web-components.md +2 -15
  27. package/skills/verify-pixels/SKILL.md +30 -44
  28. package/templates/engine/base/package.json +3 -3
  29. package/templates/react/ar-placement/src/App.tsx +29 -21
  30. package/templates/react/base/package.json +8 -8
  31. package/templates/react/sprite-game/src/App.tsx +21 -13
  32. package/templates/web-components/base/package.json +4 -4
  33. package/templates/web-components/interactive-sphere/index.html +1 -1
  34. package/templates/web-components/splat-viewer/index.html +1 -1
  35. package/skills/build-hud/SKILL.md +0 -38
  36. package/skills/manage-game-state/SKILL.md +0 -48
@@ -1,81 +1,40 @@
1
1
  ---
2
2
  name: inspect-glb
3
- description: Use before loading, placing, scaling, or animating GLB assets in PlayCanvas to measure bounds, grounding offset, clips, joints, and hierarchy offline with the bundled zero-dependency inspector.
3
+ description: Use before loading, placing, scaling, or animating GLB models in PlayCanvas to measure bounds, grounding, clips, joints, and hierarchy with the bundled offline inspector.
4
4
  ---
5
5
 
6
6
  # Inspect GLBs
7
7
 
8
- Resolve `scripts/inspect.mjs` relative to this skill directory and run it before choosing entity
9
- transforms or animation names:
8
+ Resolve `scripts/inspect.mjs` relative to this skill and run it before choosing transforms or
9
+ animation names. Shortlist files; do not dump a whole asset pack into context.
10
10
 
11
11
  ```sh
12
- node <skill-directory>/scripts/inspect.mjs public/models/Character.glb
13
- node <skill-directory>/scripts/inspect.mjs public/models/{Ship,Enemy}_*.glb
12
+ node <skill-directory>/scripts/inspect.mjs public/models/*.glb
14
13
  ```
15
14
 
16
- The output provides:
15
+ ## Read the output
17
16
 
18
- - `dims` for calculating a uniform scale;
19
- - `groundOffset` for the model wrapper's local Y position;
20
- - `center` for the pivot offset from the geometry centre: a non-zero X or Z means the root's
21
- position is not where the model appears, and Y feeds grounding;
22
- - `boundsSource` and `boundsPose` for proving how and at which pose the bounds were measured;
23
- - `requiresRuntimeCheck` for animated morphs, skins, and incomplete decoding;
24
- - `nodePaths`, `clips`, `joints`, and `animationTargets` for animation setup and rig comparison;
25
- - `morphed`, `morphAnimated`, and `skinned` for pose-sensitive assets.
17
+ - `dims`, `center`, and `groundOffset` give the uniform scale input, the pivot offset, and the Y
18
+ seat. Non-zero `center.x` or `center.z` means the model does not sit on its root.
19
+ - `boundsSource`: trust bounds for placement only when it is `vertices`. `accessor-minmax` can
20
+ over-estimate rotated nodes by up to 41 percent; `incomplete` or `null` means do not calibrate.
21
+ `boundsNotes` gives the cause.
22
+ - `boundsPose`: `static` and `default-morph` are the authored pose; `bind` is the skinned rest pose.
23
+ - `requiresRuntimeCheck` flags morphs, skins, or partial decoding; confirm extents in the app.
24
+ - `nodePaths`, `clips`, `joints`, and `animationTargets` feed `configure-animation`.
26
25
 
27
- ## Bounds precision
26
+ Bounds cannot prove facing. Confirm it in the running app and record the yaw with
27
+ `calibrate-model`. Runtime `meshInstance.aabb` expands conservatively for morphs, so place static
28
+ models from inspected bounds.
28
29
 
29
- `aabb`, `dims`, `center`, and `groundOffset` are decoded from actual vertex positions, default morph
30
- weights, and each node's world matrix. Confirm both source and pose before relying on them:
30
+ ## Compressed files
31
31
 
32
- - `vertices` means every primitive was decoded and the bounds are exact for `boundsPose`.
33
- - `accessor-minmax` means at least one primitive could not be decoded, so its bounds fall back to the
34
- node-transformed local box. That over-estimates a mesh which does not fill its own box on a node
35
- rotated off-axis, by up to 41 percent for a 45-degree yaw. `boundsNotes` gives the cause.
36
- - `incomplete` means some visible default-pose geometry could not be measured; do not calibrate from
37
- the reported bounds.
38
- - `null` means no usable bounds could be recovered.
39
-
40
- `static` and `default-morph` bounds describe visible geometry at its authored default pose. `bind`
41
- describes skinned vertices before animation. When `requiresRuntimeCheck` is true, confirm grounding
42
- and maximum animated extent in the running application even if `boundsSource` is `vertices`.
43
-
44
- Carry `boundsSource`, `aabb`, `dims`, `center`, and `groundOffset` into the asset tuning record.
45
- `groundOffset` seats the base in Y; `center.x` and `center.z` are the horizontal pivot offset that
46
- `calibrate-model` must compensate, so a placed root sits where the mesh appears, not where the
47
- authored pivot happens to be. Running the inspector alone is not placement evidence. Its global AABB supports plane grounding and
48
- broad-phase spacing, but cannot locate a non-planar support surface or attachment point. Use an
49
- authored mount point or confirm the support in the running application before placing another model.
50
-
51
- ## Extract from compressed containers
52
-
53
- The bundled inspector decodes plain, interleaved, quantized, and sparse vertex buffers. For Draco,
54
- Meshopt, or external buffers, `boundsNotes` names the cause. Parse and rewrite a scratch copy with
55
- glTF Transform's lossless `copy` command, then inspect that copy rather than shipping it:
32
+ The inspector decodes plain, interleaved, quantized, and sparse buffers. For Draco, Meshopt, or
33
+ external buffers, inspect a lossless scratch copy; do not use `optimize`, which applies extra
34
+ transforms:
56
35
 
57
36
  ```sh
58
37
  npx --yes @gltf-transform/cli@4.4.2 copy in.glb scratch.glb
59
- node <skill-directory>/scripts/inspect.mjs scratch.glb
60
38
  ```
61
39
 
62
- `copy` preserves decoded vertex values, scenes, nodes, and materials while removing Draco and
63
- Meshopt compression. Existing lossy compression may already have quantized the source. Do not
64
- substitute `optimize`: it enables additional transforms unless each one is understood and disabled.
65
-
66
- The Engine has no `EXT_meshopt_compression` support, so a meshopt asset will not render as shipped.
67
- If it must appear in the scene, transcode it to an uncompressed GLB the same way — subject to the
68
- project's rules on modifying assets — so it both loads and measures.
69
-
70
- These geometry-fit bounds can intentionally disagree with the Engine's runtime culling AABB.
71
- `MeshInstance.aabb` transforms the declared local box and expands it conservatively for morph
72
- targets, so it can be larger than visible geometry. Place static models from exact inspected bounds;
73
- confirm morph and skin extremes at runtime. A skinned mesh node's own transform is ignored here
74
- because glTF requires it; the Engine resolves to the same result.
75
-
76
- Shortlist files first; do not dump an entire asset pack into context. Bounds cannot prove facing.
77
- glTF convention is +Z forward while PlayCanvas entities face -Z, and asset packs vary. Confirm each
78
- directional model once in the running app as the `apply-conventions` skill describes, and store its
79
- yaw correction with its scale and grounding.
80
-
81
- Use `npx @gltf-transform/cli inspect` only when vertex or texture statistics justify extra tooling.
40
+ The Engine cannot load `EXT_meshopt_compression`; see `load-assets` before shipping such files.
@@ -1,94 +1,41 @@
1
1
  ---
2
2
  name: light-scene
3
- description: Use when a PlayCanvas scene must read as a deliberate cinematic image rather than a flat asset preview, to set key and ambient lighting, sky and image-based lighting, shadows, tone mapping and exposure, 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
- Prefer shipped building blocks over hand-written shaders and passes. Discover them with the
13
- `reuse-scripts` skill and adapt the closest official example with `find-examples`:
11
+ ## Defaults to change
14
12
 
15
- - the core `CameraFrame` helper for tone mapping, ambient occlusion, bloom, vignette, and grade;
16
- - a procedural sky and image-based light for ambient colour and reflections;
17
- - a shadow catcher for grounding subjects that have no lit floor.
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
- For an outdoor scene with water, stop before authoring the scene and read both `reuse-scripts` and
20
- `find-examples`. Use `graphics/water.example.mjs` as the initial composition recipe: it already
21
- joins the production `Water` script, sky, sun, `CameraFrame`, water layer, scene depth, and
22
- reflection pipeline. Get that complete frame rendering before replacing its assets or tuning its
23
- look. A custom normal-mapped material, tessellated plane, or reflection-coloured mesh is not the
24
- baseline and does not substitute for `Water` when the brief asks for reflective water.
23
+ ## Use what ships
25
24
 
26
- Keep the two named production integrations visible in the implementation:
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`.
27
33
 
28
- - import and create `Water` from `playcanvas/scripts/esm/water.mjs` on the water entity, preserving
29
- the example's required render component, camera reference, water layer, depth map, and textures;
30
- - construct `CameraFrame` from `playcanvas` for the gameplay camera, set a deliberate tone map and
31
- restrained grade, call `update()`, and destroy it with the application.
34
+ Find each integration's example with `find-examples`.
32
35
 
33
- Do not continue to cosmetic tuning while either integration is missing or diagnostics report a
34
- shader, texture, or framebuffer error.
36
+ ## Warning signs
35
37
 
36
- ## Stylized and prebaked scenes
37
-
38
- When the art direction is deliberately flat or stylized rather than photoreal, or the platform's
39
- startup, frame, or payload budget rules out the runtime pipeline above, do not treat `Water`,
40
- `CameraFrame`, and the sky/IBL bake as mandatory. The sanctioned alternative is baked static
41
- lighting via the `bake-lighting` skill plus custom material shading via the `override-shader-chunks`
42
- skill. Either way, dynamic objects and their attachments must sample or approximate the baked
43
- occlusion so they dim to the same levels as the baked surfaces, grounded with contact shadows. Fall
44
- back to the runtime pipeline above as the default path when neither trigger holds.
45
-
46
- ## Light and expose
47
-
48
- - Establish one dominant key light with a clear direction and warm or cool intent, then add soft
49
- fill or ambient so shadows are not crushed to black. A single flat ambient with no key reads as an
50
- asset gallery.
51
- - Choose a physically based tone mapping and a deliberate exposure, then balance light intensities to
52
- that exposure, not the reverse.
53
- - Drive ambient and reflections from the sky or an environment map, not a constant colour, so
54
- materials pick up their surroundings.
55
- - Tune the HDR stack in isolation before styling it. Begin with neutral tone mapping, scene exposure
56
- `1`, procedural-sky luminance near its official-example value, bloom off, grading neutral, fog off,
57
- and reflection/specular strengths at their defaults. Capture a screenshot, then change one class of
58
- value at a time. Do not simultaneously raise key intensity, sky luminance, exposure, reflection,
59
- saturation, fog brightness, and bloom.
60
- - Treat linked procedural skies as the owner of the sun light's direction, colour, and intensity.
61
- Set the light's base intensity before assigning it to the sky and tune the sky elevation/azimuth;
62
- do not also hand-author the light transform or colour as though they remained independent.
63
- - Preserve the source assets' albedo colours. If diffuse texture colours collapse toward white or a
64
- single warm tint, reduce the HDR stack before compensating with darker material colours.
65
-
66
- ## Shadow and ground
67
-
68
- - Enable shadows on the key light and size the shadow area and resolution to the framed subject, not
69
- the whole world. Soften the penumbra rather than only raising resolution.
70
- - Ground subjects with a contact or catcher shadow when the surface beneath them is not itself lit.
71
-
72
- ## Grade with restraint
73
-
74
- - Bloom, fog, vignette, and colour grade unify a frame; overdone bloom and heavy fog destroy
75
- silhouette and depth. Start subtle and increase only against the screenshot. `CameraFrame` bloom
76
- intensity is documented for the `0` to `0.1` range; stay inside it.
77
- - Match fog colour and density to the sky so the horizon stays coherent.
78
- - Preserve surface detail through the brightest reflection path. If glare clips a large part of the
79
- frame to white or a reflection becomes an opaque black blob, rebalance exposure, light intensity,
80
- reflection strength, and shadow bias before adding more post-processing.
81
- - Compare both the brightest surface and the shadow-facing side of the hero subject in the same
82
- screenshot. Reject the grade if either broad highlight clipping or crushed-black silhouette hides
83
- texture detail. A mechanically valid frame is not visually complete while either failure remains.
84
-
85
- ## Prove the look
86
-
87
- Tune against pixels, not numbers. Capture the intended framing with an image-returning browser
88
- screenshot tool and inspect the returned image, as `apply-conventions` and the local agent guide
89
- require; a saved filepath is not evidence. Confirm the subject is exposed and readable, shadows are
90
- present but not crushed, and the grade is restrained, from the real gameplay camera at the target
91
- resolution. Confirm the post-process camera keeps rendering as it orbits and pitches.
92
-
93
- Choose the authoring surface with the `build-app` skill and set lights, camera, and scene settings
94
- 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,59 +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.21'`)
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
- ## Set chunks through the documented accessor
23
-
24
- Use `material.getShaderChunks(SHADERLANGUAGE_GLSL).set(name, source)`, and the WGSL equivalent —
25
- the public, documented method — not the underlying `shaderChunks` maps, which are internal. Call
26
- `material.update()` after any change so cached shader variants are cleared.
27
-
28
- ## Emissive-only overrides
29
-
30
- An `emissivePS` override implements `void getEmission()`, writing the result into `dEmission`. When
31
- the override is meant to fully drive the visible color, zero the material's `diffuse`/`specular` and
32
- disable `useSkybox` so no other lighting term still contributes underneath it.
33
-
34
- ## Uniforms are raw
35
-
36
- `setParameter` uploads exactly what it is given, with no color-space conversion. Convert any
37
- authored sRGB color with `Color#linear()` before calling `setParameter`; an sRGB color passed
38
- straight through renders visibly darker and less saturated than it was authored.
39
-
40
- ## Keep vertex chunks instancing-aware
41
-
42
- A custom `transformCoreVS` (or any vertex chunk reading the model matrix) must keep the engine's
43
- `INSTANCING` define path — pulling the per-instance matrix under that define — with a
44
- non-instanced fallback that uses `matrix_model` directly, matching the stock chunk's structure.
45
- Dropping this path silently breaks instancing for any mesh instance using the material.
46
-
47
- ## Provide GLSL and WGSL, or pin to WebGL
48
-
49
- Override both `SHADERLANGUAGE_GLSL` and `SHADERLANGUAGE_WGSL` chunks for anything touched. On a
50
- WebGPU device, a chunk supplied only in GLSL forces the material's entire shader — not just that
51
- chunk — onto a GLSL-compiled-then-transpiled path; without the optional transpiler modules loaded
52
- (the common case), that shader fails to compile. Supply both languages, or explicitly pin the
53
- graphics device to WebGL if WGSL support is not required.
54
-
55
- ## Expand bounds for vertex displacement
56
-
57
- If the override displaces vertices, expand the mesh's bounds by the maximum displacement (see
58
- `apply-conventions`) — CPU-computed bounds never observe shader-side deformation, so culling breaks
59
- 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.22'`. 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,61 +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.drawCalls.total` or `MiniStats`; the per-pass breakdown fills only in
9
+ profiler builds. Also halve the pixel ratio and call `app.resizeCanvas()`: if frame time drops, the
10
+ cost is fill rate, not draw calls. Stop at the first rung that meets the budget and report
11
+ 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
- ## Rung 1: stop drawing invisible things
17
+ ## 2. BatchManager
19
18
 
20
- An element at opacity 0 still submits a draw call — its mesh instance exists and is still in a
21
- layer, so the GPU processes it every frame for no visible result. Toggle `enabled` on the entity to
22
- actually skip it. Check every element that is ever fully transparent, not only the ones on screen
23
- when you're profiling — this rung is easy to skip precisely because nothing looks wrong.
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`.
24
24
 
25
- ## Rung 2: `BatchManager` — merge without touching shaders
25
+ ## 3. Hardware instancing
26
26
 
27
- `app.batcher.addGroup(name, dynamic, maxAabbSize)`, then set `batchGroupId` on each member's
28
- component (render, sprite, or UI element). Use `dynamic: false` for geometry that never moves.
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.
29
30
 
30
- Contract: the `batchGroupId` setter only inserts into the batcher while `entity.enabled` is true,
31
- and that flag requires both the local enable state and hierarchy attachment — set `batchGroupId`
32
- after the entity is parented into the live tree, not before, or the member silently drops out of the
33
- group. `BatchManager.generate()` runs once automatically on the app's first rendered frame; if group
34
- membership changes afterward, call `app.batcher.markGroupDirty(id)` (or `generate([id])`) yourself.
35
- An Engine-only app without the full `Application` bootstrap must register the class via
36
- `AppOptions.batchManager` before it can batch anything.
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.
37
36
 
38
- ## Rung 3: hardware instancing — merge without touching layout
39
-
40
- Reach for this once distinct materials or per-frame transform updates would defeat `BatchManager`.
41
- Build a per-instance vertex buffer with `VertexFormat.getDefaultInstancingFormat(device)` (one mat4
42
- per instance), call `meshInstance.setInstancing(vb)`, and set `instancingCount`. A custom vertex
43
- chunk needs its own `INSTANCING` code path with an identity-matrix fallback for the non-instanced
44
- case (cross-reference `override-shader-chunks`).
45
-
46
- Culling trade-off: instanced meshes cull as one unit against a single bounding volume — an
47
- off-screen instance inside an otherwise-visible group still draws unless you opt in. Pass
48
- `setInstancing(vb, true)` and set a `RenderComponent#customAabb` spanning every instance's world
49
- extent so the renderer has something correct to cull against instead of culling nothing or
50
- culling the whole group by one instance's bounds.
51
-
52
- Adapt the official recipes rather than deriving the buffer layout or vertex-shader wiring from
53
- memory: `graphics/instancing-basic` for the format/buffer contract, `graphics/instancing-custom` for
54
- the vertex-shader side. Locate both at the installed engine version via `find-examples`.
55
-
56
- ## Prove and report
57
-
58
- Verify the rendered image is unchanged with `verify-pixels` before calling the change done —
59
- merging or instancing must not move a single pixel. Re-measure `app.stats.drawCalls` afterward and
60
- state the before/after counts in the change description; "fewer draw calls" without numbers is not
61
- 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 and reuse production scripts shipped with the installed Engine, reproduce their official example integration, and verify the result against the installed version.
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
- The installed `playcanvas` package ships production scripts under `scripts/esm/**`. Discover the
9
- current set rather than maintaining a stale list:
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,46 +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.
23
-
24
- Use a relevant shipped script as the default implementation. If it cannot own the whole feature,
25
- preserve its input signs, angular damping, bounds, and lifecycle invariants in the custom portion
26
- instead of replacing them from memory.
27
-
28
- ## Reproduce the reference integration
29
-
30
- Treat the selected script source and its closest official example as complementary references:
31
-
32
- 1. Read the source for exports, properties, defaults, fallbacks, required components, and lifecycle.
33
- 2. Read the example for assets, entity references, mesh requirements, layer ordering, scene settings,
34
- and render-pipeline setup.
35
- 3. Reproduce the smallest working baseline before customizing it. Preserve defaults that are not
36
- deliberately changed, and tune one property group at a time.
37
- 4. After a rendered frame, fail on console, shader, or missing-asset diagnostics. Exercise the
38
- behavior with real input where applicable and inspect returned screenshots from representative
39
- views at the final backbuffer density.
40
- 5. Compare the result with the official example when visual quality matters. Keep iterating or
41
- report the gap; a running fallback is not proof of correct integration.
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.
42
21
 
43
- If no matching example exists, state that and derive the integration from installed source instead
44
- of inventing it from memory.
22
+ Core exports that are not scripts include `CameraFrame` for post-processing, `Picker`
23
+ (`getSelectionAsync`), `TransformGizmo`, `OutlineRenderer`, `MiniStats`, `GltfExporter`, and
24
+ `UsdzExporter`.
45
25
 
46
- For an outdoor water scene, start from `graphics/water.example.mjs` as one integration recipe. It
47
- combines `Water`, `ProceduralSky`, `CameraControls`, `CameraFrame`, a dedicated water layer, scene
48
- depth, normal and caustics textures, and conservative rendering defaults. Establish that complete
49
- baseline before adapting its camera, assets, time of day, or water tuning.
26
+ ## Integrate
50
27
 
51
- ## 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.
52
34
 
53
- Grouped property updates differ by authoring surface. Read the selected reference and preserve
54
- 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.
55
37
 
56
- Read exactly one reference matching the code being edited:
57
- [direct Engine](references/direct-engine.md), [React](references/react.md), or
58
- [Web Components](references/web-components.md). Choose from imports and markup, not installed
59
- 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).
@@ -1,37 +1,17 @@
1
1
  # Direct Engine projects
2
2
 
3
- Import the named script class and attach it to the owning entity:
4
-
5
- ```ts
6
- import { CameraControls } from 'playcanvas/scripts/esm/camera-controls.mjs';
7
-
8
- camera.addComponent('script');
9
- const controls = camera.script?.create(CameraControls);
10
- ```
3
+ Import the selected class from its installed module, add a script component to the owning entity,
4
+ and attach it with `entity.script.create()`. Preserve an existing script component.
11
5
 
12
6
  Some production helpers are core Engine exports rather than scripts. Follow the matching example's
13
- construction style instead of forcing them through `script.create`:
14
-
15
- ```ts
16
- import { CameraFrame } from 'playcanvas';
7
+ construction and teardown contract instead of forcing them through `script.create`.
17
8
 
18
- const frame = new CameraFrame(app, camera.camera!);
19
- frame.rendering.sceneDepthMap = true;
20
- frame.update();
21
- ```
22
-
23
- Keep script constructors at module scope. `create` takes two distinct option keys that are not
24
- interchangeable: `properties` is assigned straight onto the instance, while `attributes` supplies
25
- declared `@attribute` fields. Passing the wrong one silently does nothing.
26
-
27
- Prefer mutating an existing grouped property when only a few fields change:
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.
28
12
 
29
- ```ts
30
- import { ProceduralSky } from 'playcanvas/scripts/esm/sky/procedural-sky.mjs';
31
-
32
- const sky = skyEntity.script?.create(ProceduralSky);
33
- if (sky) sky.sunLight = sun;
34
- ```
13
+ Prefer mutating an existing grouped property when only a few fields change; preserve other defaults
14
+ and follow any update or rebuild contract in the selected source.
35
15
 
36
16
  If the installed script subpaths have no declarations, declare only the patterns the project uses:
37
17
 
@@ -1,17 +1,7 @@
1
1
  # React projects
2
2
 
3
- Import the named script class and attach it through React:
4
-
5
- ```tsx
6
- import { Entity } from '@playcanvas/react';
7
- import { Camera, Script } from '@playcanvas/react/components';
8
- import { CameraControls } from 'playcanvas/scripts/esm/camera-controls.mjs';
9
-
10
- <Entity name="Camera" position={[4, 1, 4]}>
11
- <Camera />
12
- <Script script={CameraControls} />
13
- </Entity>;
14
- ```
3
+ Import the selected class from its installed module. Render `Script` from
4
+ `@playcanvas/react/components` inside the owning `Entity`, passing the class through its `script` prop.
15
5
 
16
6
  Keep imported or custom script constructors at module scope. Declaring a class inside a component
17
7
  creates a new identity on render and can recreate the script.