create-playcanvas 0.3.0 → 0.5.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/README.md +42 -25
- package/dist/index.mjs +49 -46
- package/package.json +3 -1
- package/skills/add-effects/SKILL.md +81 -0
- package/skills/apply-conventions/SKILL.md +73 -0
- package/skills/assemble-scene/SKILL.md +53 -0
- package/skills/assemble-scene/references/direct-engine.md +16 -0
- package/skills/assemble-scene/references/react.md +24 -0
- package/skills/assemble-scene/references/web-components.md +19 -0
- package/skills/build-app/SKILL.md +41 -0
- package/skills/build-app/references/direct-engine.md +12 -0
- package/skills/build-app/references/react.md +20 -0
- package/skills/build-app/references/web-components.md +55 -0
- package/skills/build-hud/SKILL.md +36 -0
- package/skills/calibrate-model/SKILL.md +65 -0
- package/skills/calibrate-model/references/direct-engine.md +22 -0
- package/skills/calibrate-model/references/react.md +27 -0
- package/skills/calibrate-model/references/web-components.md +19 -0
- package/skills/configure-animation/SKILL.md +50 -0
- package/skills/configure-animation/references/direct-engine.md +32 -0
- package/skills/configure-animation/references/react.md +32 -0
- package/skills/configure-animation/references/web-components.md +13 -0
- package/skills/find-examples/SKILL.md +79 -0
- package/skills/find-examples/references/direct-engine.md +15 -0
- package/skills/find-examples/references/react.md +17 -0
- package/skills/find-examples/references/web-components.md +17 -0
- package/skills/inspect-glb/SKILL.md +72 -0
- package/skills/inspect-glb/scripts/inspect.mjs +239 -0
- package/skills/light-scene/SKILL.md +84 -0
- package/skills/manage-game-state/SKILL.md +48 -0
- package/skills/reuse-scripts/SKILL.md +59 -0
- package/skills/reuse-scripts/references/direct-engine.md +41 -0
- package/skills/reuse-scripts/references/react.md +28 -0
- package/skills/reuse-scripts/references/web-components.md +27 -0
- package/templates/_shared/model-viewer/public/playcanvas-cube.glb +0 -0
- package/templates/engine/base/.prettierignore +2 -0
- package/templates/{vanilla-ts → engine/base}/README.md +7 -19
- package/templates/engine/base/eslint.config.mjs +3 -0
- package/templates/{vanilla-ts → engine/base}/src/vite-env.d.ts +6 -0
- package/templates/{vanilla-ts → engine/interactive-sphere}/src/main.ts +1 -1
- package/templates/{vanilla-ts → engine/interactive-sphere}/src/style.css +1 -1
- package/templates/{vanilla-ts → engine/interactive-sphere}/src/utils.ts +0 -1
- package/templates/engine/model-viewer/index.html +13 -0
- package/templates/engine/model-viewer/src/main.ts +81 -0
- package/templates/engine/model-viewer/src/style.css +8 -0
- package/templates/engine/spinning-cube/index.html +13 -0
- package/templates/engine/spinning-cube/src/main.ts +65 -0
- package/templates/engine/spinning-cube/src/style.css +8 -0
- package/templates/engine/splat-viewer/index.html +13 -0
- package/templates/engine/splat-viewer/src/main.ts +74 -0
- package/templates/engine/splat-viewer/src/style.css +8 -0
- package/templates/index.ts +37 -5
- package/templates/react/base/.prettierignore +2 -0
- package/templates/{react-ts → react/base}/README.md +6 -19
- package/templates/{react-ts → react/base}/eslint.config.mjs +1 -0
- package/templates/{react-ts → react/base}/src/vite-env.d.ts +6 -0
- package/templates/{react-ts → react/interactive-sphere}/src/Scene.tsx +1 -2
- package/templates/react/model-viewer/index.html +13 -0
- package/templates/react/model-viewer/src/App.tsx +43 -0
- package/templates/react/model-viewer/src/index.css +9 -0
- package/templates/react/model-viewer/src/main.tsx +11 -0
- package/templates/react/spinning-cube/index.html +13 -0
- package/templates/react/spinning-cube/src/App.tsx +36 -0
- package/templates/react/spinning-cube/src/index.css +9 -0
- package/templates/react/spinning-cube/src/main.tsx +11 -0
- package/templates/react/splat-viewer/index.html +13 -0
- package/templates/react/splat-viewer/src/App.tsx +39 -0
- package/templates/react/splat-viewer/src/index.css +9 -0
- package/templates/react/splat-viewer/src/main.tsx +11 -0
- package/templates/web-components/base/.prettierignore +2 -0
- package/templates/web-components/base/README.md +70 -0
- package/templates/web-components/base/_gitignore +24 -0
- package/templates/web-components/base/eslint.config.mjs +3 -0
- package/templates/web-components/base/package.json +31 -0
- package/templates/web-components/base/prettier.config.mjs +1 -0
- package/templates/web-components/base/src/assets/playcanvas.png +0 -0
- package/templates/web-components/base/src/vite-env.d.ts +19 -0
- package/templates/web-components/base/tsconfig.json +25 -0
- package/templates/web-components/interactive-sphere/index.html +53 -0
- package/templates/web-components/interactive-sphere/src/assets/typescript.svg +1 -0
- package/templates/web-components/interactive-sphere/src/main.ts +76 -0
- package/templates/web-components/interactive-sphere/src/style.css +111 -0
- package/templates/web-components/model-viewer/index.html +34 -0
- package/templates/web-components/model-viewer/src/main.ts +26 -0
- package/templates/web-components/model-viewer/src/style.css +8 -0
- package/templates/web-components/spinning-cube/index.html +29 -0
- package/templates/web-components/spinning-cube/src/main.ts +20 -0
- package/templates/web-components/spinning-cube/src/style.css +8 -0
- package/templates/web-components/splat-viewer/index.html +31 -0
- package/templates/web-components/splat-viewer/src/main.ts +14 -0
- package/templates/web-components/splat-viewer/src/style.css +8 -0
- package/templates/vanilla-ts/eslint.config.mjs +0 -3
- package/templates/vanilla-ts/public/environment-map.png +0 -0
- /package/templates/{react-ts → _shared/interactive-sphere}/public/environment-map.png +0 -0
- /package/templates/{react-ts → engine/base}/_gitignore +0 -0
- /package/templates/{vanilla-ts → engine/base}/package.json +0 -0
- /package/templates/{react-ts → engine/base}/prettier.config.mjs +0 -0
- /package/templates/{react-ts → engine/base}/src/assets/playcanvas.png +0 -0
- /package/templates/{vanilla-ts → engine/base}/tsconfig.json +0 -0
- /package/templates/{vanilla-ts → engine/interactive-sphere}/index.html +0 -0
- /package/templates/{vanilla-ts → engine/interactive-sphere}/src/App.ts +0 -0
- /package/templates/{vanilla-ts → engine/interactive-sphere}/src/assets/typescript.svg +0 -0
- /package/templates/{vanilla-ts → react/base}/_gitignore +0 -0
- /package/templates/{react-ts → react/base}/package.json +0 -0
- /package/templates/{vanilla-ts → react/base}/prettier.config.mjs +0 -0
- /package/templates/{vanilla-ts → react/base}/src/assets/playcanvas.png +0 -0
- /package/templates/{react-ts → react/base}/tsconfig.json +0 -0
- /package/templates/{react-ts → react/base}/vite.config.ts +0 -0
- /package/templates/{react-ts → react/interactive-sphere}/index.html +0 -0
- /package/templates/{react-ts → react/interactive-sphere}/src/App.css +0 -0
- /package/templates/{react-ts → react/interactive-sphere}/src/App.tsx +0 -0
- /package/templates/{react-ts → react/interactive-sphere}/src/assets/react.svg +0 -0
- /package/templates/{react-ts → react/interactive-sphere}/src/index.css +0 -0
- /package/templates/{react-ts → react/interactive-sphere}/src/main.tsx +0 -0
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# React projects
|
|
2
|
+
|
|
3
|
+
Drop graphics-device, application, component-system, and resource-handler setup because
|
|
4
|
+
`<Application>` owns it.
|
|
5
|
+
|
|
6
|
+
- Express entities and components with `<Entity>` and exports from `@playcanvas/react/components`.
|
|
7
|
+
- Load assets with hooks such as `useModel`, `useTexture`, and `useEnvAtlas` when available in the
|
|
8
|
+
installed package.
|
|
9
|
+
- Attach Engine `Script` subclasses with React's `<Script script={Type} />`.
|
|
10
|
+
- Put frame callbacks in a `Script` or `useAppEvent('update', cb)` instead of updating React state
|
|
11
|
+
every frame.
|
|
12
|
+
- Use `useApp()` only when no declarative component covers the Engine API. Clean up resources in the
|
|
13
|
+
effect that created them.
|
|
14
|
+
- Keep hooks unconditional and asset URLs stable.
|
|
15
|
+
|
|
16
|
+
Verify component and hook names against the installed `@playcanvas/react` declarations before
|
|
17
|
+
translating the example.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Web Components projects
|
|
2
|
+
|
|
3
|
+
A `playcanvas/web-components` example needs no translation: it is already a `pc-*` document, so copy
|
|
4
|
+
its markup and adjust asset paths and attributes.
|
|
5
|
+
|
|
6
|
+
Translating an Engine example instead, keep its feature options, assets, materials, shaders, and
|
|
7
|
+
update logic. Drop its graphics device and application bootstrap because `<pc-app>` owns them.
|
|
8
|
+
|
|
9
|
+
- Express supported entities and components with `pc-*` elements.
|
|
10
|
+
- Register assets as direct children of `<pc-app>` with `<pc-asset>`.
|
|
11
|
+
- Put per-entity behavior in an Engine `Script` loaded through `<pc-asset>` and attached with
|
|
12
|
+
`<pc-scripts>` and `<pc-script>`.
|
|
13
|
+
- Use `whenReady` for page-level glue or Engine APIs with no declarative element.
|
|
14
|
+
- Fetch example assets from the same Engine tag as the source example.
|
|
15
|
+
|
|
16
|
+
Verify element and attribute names against the installed `@playcanvas/web-components` package. Do
|
|
17
|
+
not translate an unsupported Engine component into a guessed custom element.
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
---
|
|
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.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Inspect GLBs
|
|
7
|
+
|
|
8
|
+
Resolve `scripts/inspect.mjs` relative to this skill directory and run it before choosing entity
|
|
9
|
+
transforms or animation names:
|
|
10
|
+
|
|
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
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
The output provides:
|
|
17
|
+
|
|
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` for proving how precisely the bounds were measured;
|
|
23
|
+
- `nodePaths`, `clips`, `joints`, and `animationTargets` for animation setup and rig comparison;
|
|
24
|
+
- `skinned` to flag bind-pose bounds that need runtime confirmation.
|
|
25
|
+
|
|
26
|
+
## Bounds precision
|
|
27
|
+
|
|
28
|
+
`aabb`, `dims`, `center`, and `groundOffset` are decoded from actual vertex positions and transformed
|
|
29
|
+
by each node's world matrix, so they are exact and can place meshes touching without a gap. Confirm
|
|
30
|
+
`boundsSource` before relying on that precision:
|
|
31
|
+
|
|
32
|
+
- `vertices` means every primitive was decoded and the bounds are exact.
|
|
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
|
+
|
|
37
|
+
Carry `boundsSource`, `aabb`, `dims`, `center`, and `groundOffset` into the asset tuning record.
|
|
38
|
+
`groundOffset` seats the base in Y; `center.x` and `center.z` are the horizontal pivot offset that
|
|
39
|
+
`calibrate-model` must compensate, so a placed root sits where the mesh appears, not where the
|
|
40
|
+
authored pivot happens to be. Running the inspector alone is not placement evidence. Its global AABB supports plane grounding and
|
|
41
|
+
broad-phase spacing, but cannot locate a non-planar support surface or attachment point. Use an
|
|
42
|
+
authored mount point or confirm the support in the running application before placing another model.
|
|
43
|
+
|
|
44
|
+
## Extract from any container with gltf-transform
|
|
45
|
+
|
|
46
|
+
The bundled inspector decodes plain vertex buffers only. When a primitive is draco- or
|
|
47
|
+
meshopt-compressed, sparse, or in an external buffer it falls back to `accessor-minmax` or reports
|
|
48
|
+
nothing, and `boundsNotes` names the cause. Use gltf-transform as the precursor that normalizes any
|
|
49
|
+
container into a plain GLB the inspector can then measure exactly. Decompress a scratch copy with the
|
|
50
|
+
passes that would alter geometry or hierarchy disabled, and inspect the copy rather than shipping it:
|
|
51
|
+
|
|
52
|
+
```sh
|
|
53
|
+
npx @gltf-transform/cli optimize in.glb scratch.glb \
|
|
54
|
+
--compress false --simplify false --flatten false --join false
|
|
55
|
+
node <skill-directory>/scripts/inspect.mjs scratch.glb
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
The Engine has no `EXT_meshopt_compression` support, so a meshopt asset will not render as shipped.
|
|
59
|
+
If it must appear in the scene, transcode it to an uncompressed GLB the same way — subject to the
|
|
60
|
+
project's rules on modifying assets — so it both loads and measures.
|
|
61
|
+
|
|
62
|
+
These exact bounds intentionally disagree with the Engine at runtime. `MeshInstance.aabb` transforms
|
|
63
|
+
the mesh's declared local box, so for a node rotated off-axis it reads larger than the geometry
|
|
64
|
+
really is. Treat that difference as expected and place from these bounds. A skinned mesh node's own
|
|
65
|
+
transform is ignored here because glTF requires it; the Engine resolves to the same result.
|
|
66
|
+
|
|
67
|
+
Shortlist files first; do not dump an entire asset pack into context. Bounds cannot prove facing.
|
|
68
|
+
glTF convention is +Z forward while PlayCanvas entities face -Z, and asset packs vary. Confirm each
|
|
69
|
+
directional model once in the running app as the `apply-conventions` skill describes, and store its
|
|
70
|
+
yaw correction with its scale and grounding.
|
|
71
|
+
|
|
72
|
+
Use `npx @gltf-transform/cli inspect` only when vertex or texture statistics justify extra tooling.
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// zero-dep glb metadata inspector. bounds come from decoded vertex positions, not accessor min/max,
|
|
3
|
+
// so a rotated node reports its true extent and models can be placed touching.
|
|
4
|
+
import fs from 'node:fs';
|
|
5
|
+
|
|
6
|
+
const GLB_MAGIC = 0x46546c67;
|
|
7
|
+
const JSON_CHUNK = 0x4e4f534a;
|
|
8
|
+
const BIN_CHUNK = 0x004e4942;
|
|
9
|
+
const IDENTITY = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1];
|
|
10
|
+
|
|
11
|
+
// componentType -> byte size, signedness and normalized divisor (glTF 2.0 table 3.1 / 5.1)
|
|
12
|
+
const COMPONENTS = {
|
|
13
|
+
5120: { size: 1, signed: true, norm: 127, read: (b, o) => b.readInt8(o) },
|
|
14
|
+
5121: { size: 1, signed: false, norm: 255, read: (b, o) => b.readUInt8(o) },
|
|
15
|
+
5122: { size: 2, signed: true, norm: 32767, read: (b, o) => b.readInt16LE(o) },
|
|
16
|
+
5123: { size: 2, signed: false, norm: 65535, read: (b, o) => b.readUInt16LE(o) },
|
|
17
|
+
5125: { size: 4, signed: false, norm: 4294967295, read: (b, o) => b.readUInt32LE(o) },
|
|
18
|
+
5126: { size: 4, signed: true, norm: 1, read: (b, o) => b.readFloatLE(o) }
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const mul = (a, b) => {
|
|
22
|
+
const o = new Array(16);
|
|
23
|
+
for (let c = 0; c < 4; c++) {
|
|
24
|
+
for (let r = 0; r < 4; r++) {
|
|
25
|
+
o[c * 4 + r] =
|
|
26
|
+
a[r] * b[c * 4] + a[4 + r] * b[c * 4 + 1] + a[8 + r] * b[c * 4 + 2] + a[12 + r] * b[c * 4 + 3];
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return o;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const compose = ([tx, ty, tz], [x, y, z, w], [sx, sy, sz]) => {
|
|
33
|
+
const x2 = x + x,
|
|
34
|
+
y2 = y + y,
|
|
35
|
+
z2 = z + z;
|
|
36
|
+
const xx = x * x2,
|
|
37
|
+
xy = x * y2,
|
|
38
|
+
xz = x * z2;
|
|
39
|
+
const yy = y * y2,
|
|
40
|
+
yz = y * z2,
|
|
41
|
+
zz = z * z2;
|
|
42
|
+
const wx = w * x2,
|
|
43
|
+
wy = w * y2,
|
|
44
|
+
wz = w * z2;
|
|
45
|
+
return [
|
|
46
|
+
(1 - (yy + zz)) * sx,
|
|
47
|
+
(xy + wz) * sx,
|
|
48
|
+
(xz - wy) * sx,
|
|
49
|
+
0,
|
|
50
|
+
(xy - wz) * sy,
|
|
51
|
+
(1 - (xx + zz)) * sy,
|
|
52
|
+
(yz + wx) * sy,
|
|
53
|
+
0,
|
|
54
|
+
(xz + wy) * sz,
|
|
55
|
+
(yz - wx) * sz,
|
|
56
|
+
(1 - (xx + yy)) * sz,
|
|
57
|
+
0,
|
|
58
|
+
tx,
|
|
59
|
+
ty,
|
|
60
|
+
tz,
|
|
61
|
+
1
|
|
62
|
+
];
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const xform = (m, [x, y, z]) => [
|
|
66
|
+
m[0] * x + m[4] * y + m[8] * z + m[12],
|
|
67
|
+
m[1] * x + m[5] * y + m[9] * z + m[13],
|
|
68
|
+
m[2] * x + m[6] * y + m[10] * z + m[14]
|
|
69
|
+
];
|
|
70
|
+
|
|
71
|
+
// + 0 normalizes -0 so a floored model reports 0, not -0
|
|
72
|
+
const round = (v) => Math.round(v * 1e4) / 1e4 + 0;
|
|
73
|
+
|
|
74
|
+
// buffer 0 of a GLB is the BIN chunk; other buffers are external files or data URIs
|
|
75
|
+
const bufferBytes = (g, bin, index) => {
|
|
76
|
+
const uri = g.buffers?.[index]?.uri;
|
|
77
|
+
if (uri === undefined) return bin;
|
|
78
|
+
const base64 = /^data:[^,]*;base64,(.*)$/s.exec(uri);
|
|
79
|
+
return base64 ? Buffer.from(base64[1], 'base64') : null;
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
// widen min/max with every decoded POSITION transformed by matrix. returns null on success,
|
|
83
|
+
// otherwise a reason the vertices could not be read so the caller can fall back and say so.
|
|
84
|
+
const accumulate = (g, bin, accessor, matrix, min, max) => {
|
|
85
|
+
const acc = g.accessors?.[accessor];
|
|
86
|
+
if (!acc) return 'no-position-accessor';
|
|
87
|
+
if (acc.sparse) return 'sparse-accessor';
|
|
88
|
+
if (acc.bufferView === undefined) return 'no-vertex-data';
|
|
89
|
+
const comp = COMPONENTS[acc.componentType];
|
|
90
|
+
if (!comp || acc.type !== 'VEC3') return 'unsupported-accessor';
|
|
91
|
+
const view = g.bufferViews?.[acc.bufferView];
|
|
92
|
+
if (!view) return 'unsupported-accessor';
|
|
93
|
+
if (view.extensions?.EXT_meshopt_compression) return 'meshopt-compressed';
|
|
94
|
+
const bytes = bufferBytes(g, bin, view.buffer ?? 0);
|
|
95
|
+
if (!bytes) return 'external-buffer';
|
|
96
|
+
const stride = view.byteStride ?? comp.size * 3;
|
|
97
|
+
const base = (view.byteOffset ?? 0) + (acc.byteOffset ?? 0);
|
|
98
|
+
if (!acc.count || base + (acc.count - 1) * stride + comp.size * 3 > bytes.length) return 'truncated-buffer';
|
|
99
|
+
const scale = acc.normalized ? comp.norm : 1;
|
|
100
|
+
for (let v = 0; v < acc.count; v++) {
|
|
101
|
+
const o = base + v * stride;
|
|
102
|
+
let x = comp.read(bytes, o) / scale;
|
|
103
|
+
let y = comp.read(bytes, o + comp.size) / scale;
|
|
104
|
+
let z = comp.read(bytes, o + comp.size * 2) / scale;
|
|
105
|
+
if (acc.normalized && comp.signed) {
|
|
106
|
+
if (x < -1) x = -1;
|
|
107
|
+
if (y < -1) y = -1;
|
|
108
|
+
if (z < -1) z = -1;
|
|
109
|
+
}
|
|
110
|
+
// inlined to avoid allocating a vector per vertex on million-vertex meshes
|
|
111
|
+
const px = matrix[0] * x + matrix[4] * y + matrix[8] * z + matrix[12];
|
|
112
|
+
const py = matrix[1] * x + matrix[5] * y + matrix[9] * z + matrix[13];
|
|
113
|
+
const pz = matrix[2] * x + matrix[6] * y + matrix[10] * z + matrix[14];
|
|
114
|
+
if (px < min[0]) min[0] = px;
|
|
115
|
+
if (py < min[1]) min[1] = py;
|
|
116
|
+
if (pz < min[2]) min[2] = pz;
|
|
117
|
+
if (px > max[0]) max[0] = px;
|
|
118
|
+
if (py > max[1]) max[1] = py;
|
|
119
|
+
if (pz > max[2]) max[2] = pz;
|
|
120
|
+
}
|
|
121
|
+
return null;
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
// loose fallback: bound the 8 corners of the accessor's local box. over-estimates a mesh that does
|
|
125
|
+
// not fill its box when the node rotation is not a multiple of 90 degrees.
|
|
126
|
+
const accumulateCorners = (acc, matrix, min, max) => {
|
|
127
|
+
if (!acc?.min || !acc?.max) return;
|
|
128
|
+
for (let c = 0; c < 8; c++) {
|
|
129
|
+
const point = xform(matrix, [
|
|
130
|
+
c & 1 ? acc.max[0] : acc.min[0],
|
|
131
|
+
c & 2 ? acc.max[1] : acc.min[1],
|
|
132
|
+
c & 4 ? acc.max[2] : acc.min[2]
|
|
133
|
+
]);
|
|
134
|
+
for (let k = 0; k < 3; k++) {
|
|
135
|
+
if (point[k] < min[k]) min[k] = point[k];
|
|
136
|
+
if (point[k] > max[k]) max[k] = point[k];
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
export const inspectGlb = (buf) => {
|
|
142
|
+
if (buf.length < 20 || buf.readUInt32LE(0) !== GLB_MAGIC || buf.readUInt32LE(16) !== JSON_CHUNK) {
|
|
143
|
+
throw new Error('not a GLB (or JSON chunk missing)');
|
|
144
|
+
}
|
|
145
|
+
const jsonLength = buf.readUInt32LE(12);
|
|
146
|
+
const g = JSON.parse(buf.toString('utf8', 20, 20 + jsonLength));
|
|
147
|
+
const binAt = 20 + jsonLength;
|
|
148
|
+
const bin =
|
|
149
|
+
binAt + 8 <= buf.length && buf.readUInt32LE(binAt + 4) === BIN_CHUNK
|
|
150
|
+
? buf.subarray(binAt + 8, binAt + 8 + buf.readUInt32LE(binAt))
|
|
151
|
+
: null;
|
|
152
|
+
const nodes = g.nodes ?? [];
|
|
153
|
+
const nodeName = (i) => nodes[i]?.name ?? `node_${i}`;
|
|
154
|
+
const parents = new Array(nodes.length).fill(-1);
|
|
155
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
156
|
+
for (const child of nodes[i].children ?? []) parents[child] = i;
|
|
157
|
+
}
|
|
158
|
+
const nodePath = (i) => {
|
|
159
|
+
const names = [];
|
|
160
|
+
for (; i >= 0; i = parents[i]) names.unshift(nodeName(i));
|
|
161
|
+
return names.join('/');
|
|
162
|
+
};
|
|
163
|
+
const children = new Set(nodes.flatMap((n) => n.children ?? []));
|
|
164
|
+
const roots = g.scenes?.[g.scene ?? 0]?.nodes ?? nodes.map((_, i) => i).filter((i) => !children.has(i));
|
|
165
|
+
|
|
166
|
+
const world = new Array(nodes.length);
|
|
167
|
+
const walk = (i, parent) => {
|
|
168
|
+
const n = nodes[i];
|
|
169
|
+
const local = n.matrix ?? compose(n.translation ?? [0, 0, 0], n.rotation ?? [0, 0, 0, 1], n.scale ?? [1, 1, 1]);
|
|
170
|
+
world[i] = mul(parent, local);
|
|
171
|
+
for (const child of n.children ?? []) walk(child, world[i]);
|
|
172
|
+
};
|
|
173
|
+
for (const root of roots) walk(root, IDENTITY);
|
|
174
|
+
|
|
175
|
+
const min = [Infinity, Infinity, Infinity];
|
|
176
|
+
const max = [-Infinity, -Infinity, -Infinity];
|
|
177
|
+
const notes = new Set();
|
|
178
|
+
let meshNodes = 0;
|
|
179
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
180
|
+
if (nodes[i].mesh === undefined || !world[i]) continue;
|
|
181
|
+
meshNodes++;
|
|
182
|
+
// glTF 2.0: "the transform of the skinned mesh node MUST be ignored" - joints drive the pose
|
|
183
|
+
const matrix = nodes[i].skin === undefined ? world[i] : IDENTITY;
|
|
184
|
+
for (const prim of g.meshes?.[nodes[i].mesh]?.primitives ?? []) {
|
|
185
|
+
const note = prim.extensions?.KHR_draco_mesh_compression
|
|
186
|
+
? 'draco-compressed'
|
|
187
|
+
: accumulate(g, bin, prim.attributes?.POSITION, matrix, min, max);
|
|
188
|
+
if (!note) continue;
|
|
189
|
+
notes.add(note);
|
|
190
|
+
accumulateCorners(g.accessors?.[prim.attributes?.POSITION], matrix, min, max);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const ok = min[0] !== Infinity;
|
|
195
|
+
return {
|
|
196
|
+
aabb: ok ? { min: min.map(round), max: max.map(round) } : null,
|
|
197
|
+
dims: ok ? max.map((v, i) => round(v - min[i])) : null,
|
|
198
|
+
center: ok ? max.map((v, i) => round((v + min[i]) / 2)) : null,
|
|
199
|
+
groundOffset: ok ? round(-min[1]) : null,
|
|
200
|
+
boundsSource: ok ? (notes.size ? 'accessor-minmax' : 'vertices') : null,
|
|
201
|
+
...(notes.size ? { boundsNotes: [...notes].sort() } : {}),
|
|
202
|
+
nodes: nodes.length,
|
|
203
|
+
nodePaths: nodes.map((_, i) => nodePath(i)).sort(),
|
|
204
|
+
meshNodes,
|
|
205
|
+
materials: (g.materials ?? []).map((m, i) => m.name ?? `material_${i}`),
|
|
206
|
+
clips: (g.animations ?? []).map((a, i) => ({
|
|
207
|
+
name: a.name ?? `clip_${i}`,
|
|
208
|
+
duration: round(Math.max(0, ...a.samplers.map((s) => g.accessors?.[s.input]?.max?.[0] ?? 0)))
|
|
209
|
+
})),
|
|
210
|
+
joints: [...new Set((g.skins ?? []).flatMap((s) => s.joints ?? []).map(nodeName))].sort(),
|
|
211
|
+
animationTargets: [
|
|
212
|
+
...new Set(
|
|
213
|
+
(g.animations ?? [])
|
|
214
|
+
.flatMap((a) => a.channels ?? [])
|
|
215
|
+
.flatMap((c) =>
|
|
216
|
+
c.target?.node === undefined ? [] : [`${nodePath(c.target.node)}.${c.target.path}`]
|
|
217
|
+
)
|
|
218
|
+
)
|
|
219
|
+
].sort(),
|
|
220
|
+
skinned: !!g.skins?.length
|
|
221
|
+
};
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
if (import.meta.main ?? import.meta.filename === process.argv[1]) {
|
|
225
|
+
const files = process.argv.slice(2);
|
|
226
|
+
if (!files.length) {
|
|
227
|
+
console.error('usage: node inspect.mjs <file.glb> [more.glb ...]');
|
|
228
|
+
process.exit(1);
|
|
229
|
+
}
|
|
230
|
+
// report per-file failures so one unreadable file cannot discard a whole glob
|
|
231
|
+
const out = files.map((file) => {
|
|
232
|
+
try {
|
|
233
|
+
return { file, ...inspectGlb(fs.readFileSync(file)) };
|
|
234
|
+
} catch (e) {
|
|
235
|
+
return { file, error: e.message };
|
|
236
|
+
}
|
|
237
|
+
});
|
|
238
|
+
console.log(JSON.stringify(out.length === 1 ? out[0] : out, null, 2));
|
|
239
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
---
|
|
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.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Cinematic rendering
|
|
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.
|
|
11
|
+
|
|
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`:
|
|
14
|
+
|
|
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.
|
|
18
|
+
|
|
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.
|
|
25
|
+
|
|
26
|
+
Keep the two named production integrations visible in the implementation:
|
|
27
|
+
|
|
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.
|
|
32
|
+
|
|
33
|
+
Do not continue to cosmetic tuning while either integration is missing or diagnostics report a
|
|
34
|
+
shader, texture, or framebuffer error.
|
|
35
|
+
|
|
36
|
+
## Light and expose
|
|
37
|
+
|
|
38
|
+
- Establish one dominant key light with a clear direction and warm or cool intent, then add soft
|
|
39
|
+
fill or ambient so shadows are not crushed to black. A single flat ambient with no key reads as an
|
|
40
|
+
asset gallery.
|
|
41
|
+
- Choose a physically based tone mapping and a deliberate exposure, then balance light intensities to
|
|
42
|
+
that exposure, not the reverse.
|
|
43
|
+
- Drive ambient and reflections from the sky or an environment map, not a constant colour, so
|
|
44
|
+
materials pick up their surroundings.
|
|
45
|
+
- Tune the HDR stack in isolation before styling it. Begin with neutral tone mapping, scene exposure
|
|
46
|
+
`1`, procedural-sky luminance near its official-example value, bloom off, grading neutral, fog off,
|
|
47
|
+
and reflection/specular strengths at their defaults. Capture a screenshot, then change one class of
|
|
48
|
+
value at a time. Do not simultaneously raise key intensity, sky luminance, exposure, reflection,
|
|
49
|
+
saturation, fog brightness, and bloom.
|
|
50
|
+
- Treat linked procedural skies as the owner of the sun light's direction, colour, and intensity.
|
|
51
|
+
Set the light's base intensity before assigning it to the sky and tune the sky elevation/azimuth;
|
|
52
|
+
do not also hand-author the light transform or colour as though they remained independent.
|
|
53
|
+
- Preserve the source assets' albedo colours. If diffuse texture colours collapse toward white or a
|
|
54
|
+
single warm tint, reduce the HDR stack before compensating with darker material colours.
|
|
55
|
+
|
|
56
|
+
## Shadow and ground
|
|
57
|
+
|
|
58
|
+
- Enable shadows on the key light and size the shadow area and resolution to the framed subject, not
|
|
59
|
+
the whole world. Soften the penumbra rather than only raising resolution.
|
|
60
|
+
- Ground subjects with a contact or catcher shadow when the surface beneath them is not itself lit.
|
|
61
|
+
|
|
62
|
+
## Grade with restraint
|
|
63
|
+
|
|
64
|
+
- Bloom, fog, vignette, and colour grade unify a frame; overdone bloom and heavy fog destroy
|
|
65
|
+
silhouette and depth. Start subtle and increase only against the screenshot. `CameraFrame` bloom
|
|
66
|
+
intensity is documented for the `0` to `0.1` range; stay inside it.
|
|
67
|
+
- Match fog colour and density to the sky so the horizon stays coherent.
|
|
68
|
+
- Preserve surface detail through the brightest reflection path. If glare clips a large part of the
|
|
69
|
+
frame to white or a reflection becomes an opaque black blob, rebalance exposure, light intensity,
|
|
70
|
+
reflection strength, and shadow bias before adding more post-processing.
|
|
71
|
+
- Compare both the brightest surface and the shadow-facing side of the hero subject in the same
|
|
72
|
+
screenshot. Reject the grade if either broad highlight clipping or crushed-black silhouette hides
|
|
73
|
+
texture detail. A mechanically valid frame is not visually complete while either failure remains.
|
|
74
|
+
|
|
75
|
+
## Prove the look
|
|
76
|
+
|
|
77
|
+
Tune against pixels, not numbers. Capture the intended framing with an image-returning browser
|
|
78
|
+
screenshot tool and inspect the returned image, as `apply-conventions` and the local agent guide
|
|
79
|
+
require; a saved filepath is not evidence. Confirm the subject is exposed and readable, shadows are
|
|
80
|
+
present but not crushed, and the grade is restrained, from the real gameplay camera at the target
|
|
81
|
+
resolution. Confirm the post-process camera keeps rendering as it orbits and pitches.
|
|
82
|
+
|
|
83
|
+
Choose the authoring surface with the `build-app` skill and set lights, camera, and scene settings
|
|
84
|
+
through its own primitives before reaching into the Engine.
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: manage-game-state
|
|
3
|
+
description: Use when structuring a PlayCanvas game's control flow — a small state machine such as ready, playing, paused, and over, pointer-lock capture, pausing on focus or pointer-lock loss, a full reset, and a stable clock and timestep — so the loop stays deterministic and recoverable.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Game state and loop
|
|
7
|
+
|
|
8
|
+
Model the game as a small explicit state machine with one owner of the clock. Every transition names
|
|
9
|
+
its states; behaviour, input, and the interface read the current state rather than a scatter of
|
|
10
|
+
booleans.
|
|
11
|
+
|
|
12
|
+
## States and transitions
|
|
13
|
+
|
|
14
|
+
- Enumerate the states — for example ready, playing, paused, over — and make transitions explicit
|
|
15
|
+
and total. Do not infer state from side effects.
|
|
16
|
+
- Gate the simulation on the active state: advance gameplay, timers, and physics only while playing.
|
|
17
|
+
|
|
18
|
+
## Capture input and pause
|
|
19
|
+
|
|
20
|
+
- Request pointer lock from a user gesture such as a click, never on load. Treat loss of pointer lock
|
|
21
|
+
and tab blur as a pause: freeze the simulation and the clock, and resume on the next gesture.
|
|
22
|
+
- Make `pointerlockchange` idempotent: when `document.pointerLockElement` is the canvas, transition
|
|
23
|
+
ready or paused to playing and leave playing unchanged; only transition playing to paused when the
|
|
24
|
+
element is no longer the canvas. Never implement it as a toggle. Browsers and automation can both
|
|
25
|
+
report an already-acquired lock, and a duplicate enter event must not pause the game.
|
|
26
|
+
- Clamp the per-frame delta before integrating anything, as `build-app` requires, so a backgrounded
|
|
27
|
+
tab or a slow frame cannot inject one large step into movement, cooldowns, or the clock.
|
|
28
|
+
|
|
29
|
+
## Reset
|
|
30
|
+
|
|
31
|
+
- A reset restores every owned system to its ready values — entities, camera, clock, timers,
|
|
32
|
+
cooldowns, effects, and overlays — not only the player. Route reset through the same setup the
|
|
33
|
+
initial state uses, so ready and reset cannot drift apart.
|
|
34
|
+
- Transition to `ready` explicitly before releasing pointer lock. Never derive the reset state from
|
|
35
|
+
`document.pointerLockElement`; pointer lock is an input side effect, not the state machine.
|
|
36
|
+
- Define timer semantics once and keep them literal in state and snapshots. A field named `reload`
|
|
37
|
+
is remaining cooldown time: `0` means ready, firing assigns a positive duration, and the active
|
|
38
|
+
loop counts it down to exactly `0`. Do not expose a normalized readiness value under that name.
|
|
39
|
+
|
|
40
|
+
## Prove the loop
|
|
41
|
+
|
|
42
|
+
Drive the full cycle with real input: enter play, lose pointer lock and confirm the clock and
|
|
43
|
+
simulation freeze, resume, reset and confirm every field returns to its ready value, then reach the
|
|
44
|
+
end state and restart. Read these from the app's own state snapshot, never from console mutation —
|
|
45
|
+
manufactured state is not evidence.
|
|
46
|
+
|
|
47
|
+
Choose the authoring surface with the `build-app` skill and own the loop and lifecycle in the surface
|
|
48
|
+
that creates the application.
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
---
|
|
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.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Engine scripts
|
|
7
|
+
|
|
8
|
+
The installed `playcanvas` package ships production scripts under `scripts/esm/**`. Discover the
|
|
9
|
+
current set rather than maintaining a stale list:
|
|
10
|
+
|
|
11
|
+
```sh
|
|
12
|
+
rg 'static scriptName =' node_modules/playcanvas/scripts/esm \
|
|
13
|
+
| sed "s|.*/scripts/esm/||; s|:.*static scriptName = ['\"]| -> |; s|['\"].*||" \
|
|
14
|
+
| sort
|
|
15
|
+
```
|
|
16
|
+
|
|
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.
|
|
42
|
+
|
|
43
|
+
If no matching example exists, state that and derive the integration from installed source instead
|
|
44
|
+
of inventing it from memory.
|
|
45
|
+
|
|
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.
|
|
50
|
+
|
|
51
|
+
## Preserve grouped defaults
|
|
52
|
+
|
|
53
|
+
Grouped property updates differ by authoring surface. Read the selected reference and preserve
|
|
54
|
+
defaults that are not being changed.
|
|
55
|
+
|
|
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.
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# Direct Engine projects
|
|
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
|
+
```
|
|
11
|
+
|
|
12
|
+
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';
|
|
17
|
+
|
|
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:
|
|
28
|
+
|
|
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
|
+
```
|
|
35
|
+
|
|
36
|
+
If the installed script subpaths have no declarations, declare only the patterns the project uses:
|
|
37
|
+
|
|
38
|
+
```ts
|
|
39
|
+
declare module 'playcanvas/scripts/esm/*.mjs';
|
|
40
|
+
declare module 'playcanvas/scripts/esm/*/*.mjs';
|
|
41
|
+
```
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# React projects
|
|
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
|
+
```
|
|
15
|
+
|
|
16
|
+
Keep imported or custom script constructors at module scope. Declaring a class inside a component
|
|
17
|
+
creates a new identity on render and can recreate the script.
|
|
18
|
+
|
|
19
|
+
Props on `<Script>` become script properties. Pass complete grouped properties or use a ref to
|
|
20
|
+
mutate the existing group after mount. Verify the current component props in the installed
|
|
21
|
+
`@playcanvas/react` declarations.
|
|
22
|
+
|
|
23
|
+
If the installed Engine script subpaths have no declarations, add:
|
|
24
|
+
|
|
25
|
+
```ts
|
|
26
|
+
declare module 'playcanvas/scripts/esm/*.mjs';
|
|
27
|
+
declare module 'playcanvas/scripts/esm/*/*.mjs';
|
|
28
|
+
```
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# Web Components projects
|
|
2
|
+
|
|
3
|
+
Expose an Engine script as an ES module and register it as an asset:
|
|
4
|
+
|
|
5
|
+
```html
|
|
6
|
+
<pc-app>
|
|
7
|
+
<pc-asset src="/scripts/camera-controls.mjs"></pc-asset>
|
|
8
|
+
<pc-scene>
|
|
9
|
+
<pc-entity name="Camera">
|
|
10
|
+
<pc-camera></pc-camera>
|
|
11
|
+
<pc-scripts>
|
|
12
|
+
<pc-script name="cameraControls" move-speed="4"></pc-script>
|
|
13
|
+
</pc-scripts>
|
|
14
|
+
</pc-entity>
|
|
15
|
+
</pc-scene>
|
|
16
|
+
</pc-app>
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
The `name` must equal the class's static `scriptName`. Extra kebab-case attributes map to camelCase
|
|
20
|
+
script properties. Use the `attributes` JSON attribute for nested or reserved properties; it
|
|
21
|
+
recursively merges partial grouped objects into the script's defaults. Arrays are not merged element
|
|
22
|
+
by element, so an array in the JSON replaces the script's default array whole, and a kebab-case
|
|
23
|
+
attribute always wins over the same key inside the JSON.
|
|
24
|
+
|
|
25
|
+
Use `asset:`, `entity:`, `vec2:`, `vec3:`, `vec4:`, or `color:` prefixes where a property needs a
|
|
26
|
+
typed reference. Verify the selected script's export, property names, and defaults against the
|
|
27
|
+
installed Engine file.
|
|
Binary file
|