create-playcanvas 0.6.2 → 0.7.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 +1 -1
- package/skills/apply-conventions/SKILL.md +2 -0
- package/skills/bake-lighting/SKILL.md +61 -0
- package/skills/build-hud/SKILL.md +2 -0
- package/skills/light-scene/SKILL.md +10 -0
- package/skills/override-shader-chunks/SKILL.md +59 -0
- package/skills/reduce-draw-calls/SKILL.md +61 -0
- package/skills/verify-pixels/SKILL.md +50 -0
package/package.json
CHANGED
|
@@ -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.
|