create-playcanvas 0.6.2 → 0.8.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-playcanvas",
3
- "version": "0.6.2",
3
+ "version": "0.8.0",
4
4
  "description": "Scaffold a PlayCanvas project",
5
5
  "keywords": [
6
6
  "playcanvas",
@@ -43,6 +43,8 @@ details. Apply these stable conventions throughout the project.
43
43
  - Materials cull back faces by default; counter-clockwise winding defines the front face.
44
44
  - Clone a material before per-instance tinting or edits, and assign the clone; never mutate one
45
45
  shared by other instances, including default and asset-imported materials.
46
+ - `setParameter` uploads raw values; author colours in sRGB and convert with `Color#linear()`
47
+ before upload — do not hand-roll gamma.
46
48
  - Entities do not have an `aabb`. Union descendant `meshInstance.aabb` values after transforms have
47
49
  synchronized.
48
50
  - Let `Mesh.update()` calculate local bounds. If passing `false` for `updateBoundingBox`, assign an
@@ -0,0 +1,61 @@
1
+ ---
2
+ name: bake-lighting
3
+ description: Use when a PlayCanvas scene's static lighting, shadows, or ambient occlusion costs too much per frame, or lighting must ship precomputed for startup or payload budgets.
4
+ ---
5
+
6
+ # Precompute static lighting
7
+
8
+ Static geometry lit by static lights can bake into lightmap textures once, removing per-frame
9
+ shadow-map rendering and per-pixel dynamic lighting for that light entirely.
10
+
11
+ ## Engine `Lightmapper` flags
12
+
13
+ - Engine-only bootstraps must register it: `AppOptions.lightmapper = Lightmapper`. `Application`
14
+ wires this by default; only a manual `AppOptions` assembly needs the explicit line.
15
+ - Per light: `light.bake = true`, then `light.affectDynamic = false` once every mesh it lights is
16
+ lightmapped, so it stops contributing to the real-time pass.
17
+ - Per mesh: `render.lightmapped = true` — required for the mesh to receive any light once
18
+ `affectDynamic` is false, a correctness requirement, not just an optimization —
19
+ `render.lightmapSizeMultiplier` to size its lightmap, `render.castShadowsLightmap` so lightmapped
20
+ meshes still shadow each other in the bake.
21
+ - Bake mode: `BAKE_COLOR` for flat diffuse materials; `BAKE_COLORDIR` adds a dominant-light-direction
22
+ pass for normal- or specular-mapped materials that need one.
23
+ - Baked ambient occlusion, independent of any light: `scene.ambientBake` plus
24
+ `ambientBakeNumSamples`, `ambientBakeOcclusionBrightness`/`OcclusionContrast`.
25
+ - Run `app.lightmapper.bake(null, mode)` once the scene exists; `null` bakes every lightmapped node.
26
+
27
+ ## Verify UVs before trusting this on real assets
28
+
29
+ Procedural primitives auto-generate a usable UV set; imported GLB models often do not ship a second,
30
+ non-overlapping UV channel suitable for a lightmap atlas. Read the installed engine's source for
31
+ what its `Lightmapper` actually requires — which UV set it samples, whether it unwraps one for you —
32
+ before assuming a real model will bake correctly. Do not carry a primitive-only test result into
33
+ production assets unchecked.
34
+
35
+ ## Choose the rung by budget, not by default
36
+
37
+ On-device bake cost scales with baked-node count and lightmap resolution; estimate it against the
38
+ startup budget before choosing a rung. Stay on-device unless:
39
+
40
+ - the estimated bake time does not fit the startup or first-paint budget on the target device, or
41
+ - the result needs stylized material detail beyond lighting that the runtime `Lightmapper` cannot
42
+ produce.
43
+
44
+ Only then move to an offline bake shipped as assets, following the same shape as any other
45
+ generated-asset pipeline: registry-keyed per-asset configs (a new asset is a config change, not new
46
+ pipeline code), a CLI with per-target fast paths, staged size logging, worker-thread parallelism for
47
+ build time, and a coverage self-audit that fails the bake when an expected output is missing or
48
+ empty.
49
+
50
+ ## Keep dynamic objects consistent
51
+
52
+ Whichever rung you use, dynamic objects moving through baked lighting must not read as ignoring it:
53
+ sample or approximate the baked occlusion/shadow field for them and their attachments so they dim to
54
+ the same levels as the static geometry around them, and ground them with a contact shadow. A
55
+ dynamic object whose custom shading must meet baked maps is `override-shader-chunks` territory.
56
+
57
+ ## Prove it
58
+
59
+ Capture `verify-pixels`-style frames both in and out of the baked shadow, before and after the
60
+ change, and compare them. A screenshot glanced at once is not proof the bake reproduced the lighting
61
+ it removed.
@@ -20,6 +20,8 @@ each transition; never store gameplay truth in the DOM or a UI element.
20
20
 
21
21
  - Map each overlay to a game state and each indicator to a snapshot field. A transition shows or
22
22
  hides overlays; it does not duplicate the state the game already owns (see `manage-game-state`).
23
+ - An indicator that is idle most of the time toggles `enabled` with its visibility; an element left
24
+ drawing at opacity 0 still costs a draw call every frame (see `reduce-draw-calls`).
23
25
  - Anchor and scale elements to the viewport so the layout holds at the target resolution and on
24
26
  resize. Verify at the acceptance resolution, not the default window size.
25
27
  - Keep copy minimal and legible over the scene: back text with a plate or shadow where the frame is
@@ -33,6 +33,16 @@ Keep the two named production integrations visible in the implementation:
33
33
  Do not continue to cosmetic tuning while either integration is missing or diagnostics report a
34
34
  shader, texture, or framebuffer error.
35
35
 
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
+
36
46
  ## Light and expose
37
47
 
38
48
  - Establish one dominant key light with a clear direction and warm or cool intent, then add soft
@@ -0,0 +1,59 @@
1
+ ---
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.
4
+ ---
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.
@@ -0,0 +1,61 @@
1
+ ---
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.
4
+ ---
5
+
6
+ # Cut draw calls, ranked by cost
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.
11
+
12
+ ## Measure first
13
+
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."
17
+
18
+ ## Rung 1: stop drawing invisible things
19
+
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.
24
+
25
+ ## Rung 2: `BatchManager` — merge without touching shaders
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.
29
+
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.
37
+
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.
@@ -0,0 +1,50 @@
1
+ ---
2
+ name: verify-pixels
3
+ description: Use when changing PlayCanvas rendering code that must not change the rendered image — draw-call optimization, hardware instancing, shader or material refactors, or asset pipeline swaps — to prove the output unchanged before shipping.
4
+ ---
5
+
6
+ # Prove pixels unchanged
7
+
8
+ A rendering change that must not alter the image is not proven by eyeballing a screenshot at one
9
+ pose. Build a deterministic capture, compare byte-exact counts across a pose × phase matrix, and
10
+ prove the capture harness itself is trustworthy before trusting what it reports about the change.
11
+
12
+ ## Make the frame deterministic
13
+
14
+ - Drive every animated shader or vertex effect from one app-owned time value, never `Date.now()` or
15
+ `performance.now()` read inside the render path, so a captured phase is exactly reproducible.
16
+ - Freeze the clock with `app.timeScale = 0` before capturing; nothing should advance between frames
17
+ you did not explicitly step.
18
+ - Step frames explicitly: set `app.autoRender = false` once, then set `app.renderNextFrame = true`
19
+ before each frame you want rendered. The engine renders exactly that frame and clears the flag —
20
+ do not rely on the free-running render loop plus a timed screenshot.
21
+ - Read the rendered pixels with `await device.readPixelsAsync(x, y, w, h, pixels)` against the exact
22
+ backbuffer, not a re-encoded screenshot (e.g. `canvas.toDataURL`) that can introduce compression or
23
+ colour-management differences the eye won't catch. In the installed engine this method lives on
24
+ `WebglGraphicsDevice`, not the base `GraphicsDevice` type, so narrow to it (or branch on
25
+ `device.isWebGL2`) before calling; a WebGPU project needs its own equivalent readback. If the
26
+ project already has a screenshot path, hold it to the same rule: fixed size, fixed pose, no lossy
27
+ step before the byte comparison.
28
+
29
+ ## Build a pose × phase matrix
30
+
31
+ Single-pose, single-frame proof cannot see everything a rendering refactor can break. Choose at
32
+ least two representative camera poses — angles that would expose a dropped instance, a wrong batch
33
+ bound, or a seam differently — and at least two animation phases, including one mid-animation, not
34
+ only frame zero. Capture every pose × phase pair for the build before the change and the build after
35
+ it, with the same canvas size, camera, and lighting each time.
36
+
37
+ ## Run a same-build control first
38
+
39
+ Before trusting any diff between the old and new build, capture the same pose × phase matrix twice
40
+ from the *unmodified* build. Two captures of identical, frozen state must be bit-identical. If they
41
+ are not, the capture path itself is the source of noise — an unseeded animation, an asset still
42
+ loading, a GPU timing race — and must be fixed before it can say anything about the real change.
43
+
44
+ ## Gate and report
45
+
46
+ Byte-compare each pose × phase pair between the two builds; do not diff by looking. Zero differing
47
+ pixels passes outright. Any nonzero diff must be reviewed on-screen, and its cause and extent stated
48
+ in the change description — never merged silently. Report the actual count every time, for example
49
+ "0 of 65536 pixels differ" or "312 of 65536 pixels differ, confined to the object's silhouette edge".
50
+ "Looks the same" or "no visible difference" is not a result.
@@ -15,14 +15,14 @@
15
15
  "typecheck": "tsc --noEmit"
16
16
  },
17
17
  "dependencies": {
18
- "playcanvas": "^2.21.4"
18
+ "playcanvas": "^2.22.0"
19
19
  },
20
20
  "devDependencies": {
21
21
  "@playcanvas/eslint-config": "3.0.0-beta.8",
22
22
  "eslint": "^9.39.5",
23
23
  "prettier": "^3.9.6",
24
24
  "typescript": "~6.0.3",
25
- "vite": "^8.2.1"
25
+ "vite": "^8.2.2"
26
26
  },
27
27
  "engines": {
28
28
  "node": ">=22.23.2"
@@ -16,19 +16,19 @@
16
16
  },
17
17
  "dependencies": {
18
18
  "@playcanvas/react": "^0.11.5",
19
- "playcanvas": "^2.21.4",
19
+ "playcanvas": "^2.22.0",
20
20
  "react": "^19.2.8",
21
21
  "react-dom": "^19.2.8"
22
22
  },
23
23
  "devDependencies": {
24
24
  "@playcanvas/eslint-config": "3.0.0-beta.8",
25
25
  "@types/react": "^19.2.18",
26
- "@types/react-dom": "^19.2.4",
27
- "@vitejs/plugin-react": "^6.0.5",
26
+ "@types/react-dom": "^19.2.7",
27
+ "@vitejs/plugin-react": "^6.1.1",
28
28
  "eslint": "^9.39.5",
29
29
  "prettier": "^3.9.6",
30
30
  "typescript": "~6.0.3",
31
- "vite": "^8.2.1"
31
+ "vite": "^8.2.2"
32
32
  },
33
33
  "engines": {
34
34
  "node": ">=22.23.2"
@@ -15,15 +15,15 @@
15
15
  "typecheck": "tsc --noEmit"
16
16
  },
17
17
  "dependencies": {
18
- "@playcanvas/web-components": "^0.17.0",
19
- "playcanvas": "^2.21.4"
18
+ "@playcanvas/web-components": "^0.21.0",
19
+ "playcanvas": "^2.22.0"
20
20
  },
21
21
  "devDependencies": {
22
22
  "@playcanvas/eslint-config": "3.0.0-beta.8",
23
23
  "eslint": "^9.39.5",
24
24
  "prettier": "^3.9.6",
25
25
  "typescript": "~6.0.3",
26
- "vite": "^8.2.1"
26
+ "vite": "^8.2.2"
27
27
  },
28
28
  "engines": {
29
29
  "node": ">=22.23.2"