create-playcanvas 0.6.0 → 0.6.2
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/assemble-scene/references/web-components.md +2 -2
- package/skills/build-app/references/web-components.md +4 -4
- package/skills/calibrate-model/references/direct-engine.md +7 -4
- package/skills/calibrate-model/references/react.md +8 -3
- package/skills/calibrate-model/references/web-components.md +7 -4
- package/skills/configure-animation/references/web-components.md +18 -9
- package/skills/find-examples/references/web-components.md +1 -1
- package/skills/inspect-glb/SKILL.md +27 -18
- package/skills/inspect-glb/scripts/inspect.mjs +179 -50
- package/skills/reuse-scripts/references/web-components.md +3 -3
- package/templates/web-components/base/README.md +3 -3
- package/templates/web-components/base/package.json +1 -1
- package/templates/web-components/first-person-controller/index.html +26 -26
- package/templates/web-components/first-person-controller/src/main.ts +1 -1
- package/templates/web-components/interactive-sphere/src/main.ts +1 -1
- package/templates/web-components/model-viewer/src/main.ts +1 -1
- package/templates/web-components/physics-playground/index.html +3 -3
- package/templates/web-components/physics-playground/src/main.ts +1 -1
- package/templates/web-components/product-configurator/src/main.ts +3 -1
- package/templates/web-components/spinning-cube/src/main.ts +1 -1
- package/templates/web-components/splat-viewer/src/main.ts +1 -1
- package/templates/web-components/third-person-controller/index.html +15 -15
- package/templates/web-components/third-person-controller/src/main.ts +1 -1
package/package.json
CHANGED
|
@@ -4,7 +4,7 @@ Represent the whole gameplay object below one semantic root:
|
|
|
4
4
|
|
|
5
5
|
```html
|
|
6
6
|
<pc-entity name="Enemy_1" position="4 0 -3" rotation="0 90 0">
|
|
7
|
-
<pc-
|
|
7
|
+
<pc-rigid-body type="dynamic" mass="1"></pc-rigid-body>
|
|
8
8
|
<pc-collision type="capsule"></pc-collision>
|
|
9
9
|
<pc-entity position="0 0.8 0" rotation="0 180 0" scale="0.01 0.01 0.01">
|
|
10
10
|
<pc-model asset="enemy"></pc-model>
|
|
@@ -14,6 +14,6 @@ Represent the whole gameplay object below one semantic root:
|
|
|
14
14
|
|
|
15
15
|
Keep component tags as direct children of the owning `<pc-entity>`. Register the model with a
|
|
16
16
|
direct-child `<pc-asset>` under `<pc-app>`. Before using rigid bodies, load Ammo with a direct-child
|
|
17
|
-
`<pc-
|
|
17
|
+
`<pc-wasm name="Ammo">` carrying its `glue`, `wasm`, and `fallback` attributes; `<pc-app>` awaits
|
|
18
18
|
its direct-child modules before creating the graphics device, so every body in the document depends
|
|
19
19
|
on that one element. Add and remove the semantic root as one DOM subtree.
|
|
@@ -35,10 +35,10 @@ Build supported application structure declaratively:
|
|
|
35
35
|
- Treat a present boolean attribute as true unless its value is `false`; remove it to restore the
|
|
36
36
|
Engine default.
|
|
37
37
|
|
|
38
|
-
Load Engine `Script` modules with `<pc-asset>`, then attach them through `<pc-
|
|
39
|
-
`<pc-script>`. Match the element's `name` to the class's static `scriptName`.
|
|
40
|
-
attributes map to camelCase properties; use the `attributes` JSON attribute for
|
|
41
|
-
values.
|
|
38
|
+
Load Engine `Script` modules with `<pc-asset>`, then attach them through `<pc-script>` and
|
|
39
|
+
`<pc-script-instance>`. Match the instance element's `name` to the class's static `scriptName`.
|
|
40
|
+
Extra kebab-case attributes map to camelCase properties; use the `attributes` JSON attribute for
|
|
41
|
+
nested or reserved values.
|
|
42
42
|
|
|
43
43
|
Use `whenReady` for occasional page-level Engine access:
|
|
44
44
|
|
|
@@ -11,12 +11,15 @@ const instance = (name: keyof typeof ASSET_TUNING) => {
|
|
|
11
11
|
castShadows: true
|
|
12
12
|
});
|
|
13
13
|
model.setLocalScale(t.scale, t.scale, t.scale);
|
|
14
|
-
model.setLocalPosition(0, t.y,
|
|
15
|
-
model.setLocalEulerAngles(0, t.yaw, 0);
|
|
14
|
+
model.setLocalPosition(-t.center[0] * t.scale, t.y, -t.center[2] * t.scale);
|
|
16
15
|
const root = new Entity(name);
|
|
17
|
-
|
|
16
|
+
const yaw = new Entity(`${name}-yaw`);
|
|
17
|
+
yaw.setLocalEulerAngles(0, t.yaw, 0);
|
|
18
|
+
yaw.addChild(model);
|
|
19
|
+
root.addChild(yaw);
|
|
18
20
|
return root;
|
|
19
21
|
};
|
|
20
22
|
```
|
|
21
23
|
|
|
22
|
-
Only position and rotate the returned root. Keep the
|
|
24
|
+
Only position and rotate the returned root. Keep the yaw, scale, and pivot compensation inside
|
|
25
|
+
`instance()`.
|
|
@@ -15,13 +15,18 @@ const Model = ({ id, src, heading = 0, position = [0, 0, 0] }) => {
|
|
|
15
15
|
|
|
16
16
|
return (
|
|
17
17
|
<Entity name={id} position={position} rotation={[0, heading, 0]}>
|
|
18
|
-
<Entity
|
|
19
|
-
<
|
|
18
|
+
<Entity rotation={[0, t.yaw, 0]}>
|
|
19
|
+
<Entity
|
|
20
|
+
position={[-t.center[0] * t.scale, t.y, -t.center[2] * t.scale]}
|
|
21
|
+
scale={[t.scale, t.scale, t.scale]}
|
|
22
|
+
>
|
|
23
|
+
<Render type="asset" asset={asset} />
|
|
24
|
+
</Entity>
|
|
20
25
|
</Entity>
|
|
21
26
|
</Entity>
|
|
22
27
|
);
|
|
23
28
|
};
|
|
24
29
|
```
|
|
25
30
|
|
|
26
|
-
Add project-specific prop types without changing the
|
|
31
|
+
Add project-specific prop types without changing the three-level transform structure. Keep hooks
|
|
27
32
|
unconditional and asset URLs stable.
|
|
@@ -7,13 +7,16 @@ Keep gameplay placement on the outer entity and calibration on a visual child:
|
|
|
7
7
|
<pc-asset id="ship" src="/models/ship.glb"></pc-asset>
|
|
8
8
|
<pc-scene>
|
|
9
9
|
<pc-entity name="Ship" position="12 0 -4" rotation="0 90 0">
|
|
10
|
-
<pc-entity
|
|
11
|
-
<pc-
|
|
10
|
+
<pc-entity rotation="0 180 0">
|
|
11
|
+
<pc-entity position="-0.45 1.2 0.225" scale="0.9 0.9 0.9">
|
|
12
|
+
<pc-model asset="ship"></pc-model>
|
|
13
|
+
</pc-entity>
|
|
12
14
|
</pc-entity>
|
|
13
15
|
</pc-entity>
|
|
14
16
|
</pc-scene>
|
|
15
17
|
</pc-app>
|
|
16
18
|
```
|
|
17
19
|
|
|
18
|
-
Generate repeated markup from one `{ scale, y, yaw }` record per asset.
|
|
19
|
-
space-separated and use
|
|
20
|
+
Generate repeated markup from one `{ scale, y, yaw, center }` record per asset. Calculate the visual
|
|
21
|
+
position as `-center.x * scale, y, -center.z * scale`. Keep vector attributes space-separated and use
|
|
22
|
+
degrees for rotations.
|
|
@@ -1,13 +1,22 @@
|
|
|
1
1
|
# Web Components projects
|
|
2
2
|
|
|
3
|
-
`<pc-model>`
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
Use `<pc-anim>` inside `<pc-model>` to animate the model hierarchy. Without `<pc-anim-clip>`
|
|
4
|
+
children, it assigns every animation from the model's container by track name and starts the first
|
|
5
|
+
clip. Select and cross-fade declaratively:
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
```html
|
|
8
|
+
<pc-entity>
|
|
9
|
+
<pc-model asset="character">
|
|
10
|
+
<pc-anim clip="Idle" transition-time="0.2"></pc-anim>
|
|
11
|
+
</pc-model>
|
|
12
|
+
</pc-entity>
|
|
13
|
+
```
|
|
10
14
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
15
|
+
Add `<pc-anim-clip name="...">` children to declare a subset, rename states, set per-clip `loop` or
|
|
16
|
+
`speed`, or source tracks through an explicit `asset`. Use `<pc-anim>` attributes for `activate`,
|
|
17
|
+
`clip`, `speed`, and `transition-time`; its `play`, `pause`, and `transition` methods cover imperative
|
|
18
|
+
playback.
|
|
19
|
+
|
|
20
|
+
Use a module-scoped Engine `Script` only for state graphs, retargeting, or orchestration beyond that
|
|
21
|
+
component contract. Keep scale on a visual child `<pc-entity>`, not on skinned bones, and confirm
|
|
22
|
+
playback after `pc-app` is ready.
|
|
@@ -9,7 +9,7 @@ update logic. Drop its graphics device and application bootstrap because `<pc-ap
|
|
|
9
9
|
- Express supported entities and components with `pc-*` elements.
|
|
10
10
|
- Register assets as direct children of `<pc-app>` with `<pc-asset>`.
|
|
11
11
|
- Put per-entity behavior in an Engine `Script` loaded through `<pc-asset>` and attached with
|
|
12
|
-
`<pc-
|
|
12
|
+
`<pc-script>` and `<pc-script-instance>`.
|
|
13
13
|
- Use `whenReady` for page-level glue or Engine APIs with no declarative element.
|
|
14
14
|
- Fetch example assets from the same Engine tag as the source example.
|
|
15
15
|
|
|
@@ -19,20 +19,27 @@ The output provides:
|
|
|
19
19
|
- `groundOffset` for the model wrapper's local Y position;
|
|
20
20
|
- `center` for the pivot offset from the geometry centre: a non-zero X or Z means the root's
|
|
21
21
|
position is not where the model appears, and Y feeds grounding;
|
|
22
|
-
- `boundsSource` for proving how
|
|
22
|
+
- `boundsSource` and `boundsPose` for proving how and at which pose the bounds were measured;
|
|
23
|
+
- `requiresRuntimeCheck` for animated morphs, skins, and incomplete decoding;
|
|
23
24
|
- `nodePaths`, `clips`, `joints`, and `animationTargets` for animation setup and rig comparison;
|
|
24
|
-
- `skinned`
|
|
25
|
+
- `morphed`, `morphAnimated`, and `skinned` for pose-sensitive assets.
|
|
25
26
|
|
|
26
27
|
## Bounds precision
|
|
27
28
|
|
|
28
|
-
`aabb`, `dims`, `center`, and `groundOffset` are decoded from actual vertex positions
|
|
29
|
-
|
|
30
|
-
`boundsSource` before relying on that precision:
|
|
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:
|
|
31
31
|
|
|
32
|
-
- `vertices` means every primitive was decoded and the bounds are exact
|
|
32
|
+
- `vertices` means every primitive was decoded and the bounds are exact for `boundsPose`.
|
|
33
33
|
- `accessor-minmax` means at least one primitive could not be decoded, so its bounds fall back to the
|
|
34
34
|
node-transformed local box. That over-estimates a mesh which does not fill its own box on a node
|
|
35
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`.
|
|
36
43
|
|
|
37
44
|
Carry `boundsSource`, `aabb`, `dims`, `center`, and `groundOffset` into the asset tuning record.
|
|
38
45
|
`groundOffset` seats the base in Y; `center.x` and `center.z` are the horizontal pivot offset that
|
|
@@ -41,28 +48,30 @@ authored pivot happens to be. Running the inspector alone is not placement evide
|
|
|
41
48
|
broad-phase spacing, but cannot locate a non-planar support surface or attachment point. Use an
|
|
42
49
|
authored mount point or confirm the support in the running application before placing another model.
|
|
43
50
|
|
|
44
|
-
## Extract from
|
|
51
|
+
## Extract from compressed containers
|
|
45
52
|
|
|
46
|
-
The bundled inspector decodes plain
|
|
47
|
-
|
|
48
|
-
|
|
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:
|
|
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:
|
|
51
56
|
|
|
52
57
|
```sh
|
|
53
|
-
npx @gltf-transform/cli
|
|
54
|
-
--compress false --simplify false --flatten false --join false
|
|
58
|
+
npx --yes @gltf-transform/cli@4.4.2 copy in.glb scratch.glb
|
|
55
59
|
node <skill-directory>/scripts/inspect.mjs scratch.glb
|
|
56
60
|
```
|
|
57
61
|
|
|
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
|
+
|
|
58
66
|
The Engine has no `EXT_meshopt_compression` support, so a meshopt asset will not render as shipped.
|
|
59
67
|
If it must appear in the scene, transcode it to an uncompressed GLB the same way — subject to the
|
|
60
68
|
project's rules on modifying assets — so it both loads and measures.
|
|
61
69
|
|
|
62
|
-
These
|
|
63
|
-
the
|
|
64
|
-
|
|
65
|
-
|
|
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.
|
|
66
75
|
|
|
67
76
|
Shortlist files first; do not dump an entire asset pack into context. Bounds cannot prove facing.
|
|
68
77
|
glTF convention is +Z forward while PlayCanvas entities face -Z, and asset packs vary. Confirm each
|
|
@@ -2,11 +2,14 @@
|
|
|
2
2
|
// zero-dep glb metadata inspector. bounds come from decoded vertex positions, not accessor min/max,
|
|
3
3
|
// so a rotated node reports its true extent and models can be placed touching.
|
|
4
4
|
import fs from 'node:fs';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
5
6
|
|
|
6
7
|
const GLB_MAGIC = 0x46546c67;
|
|
7
8
|
const JSON_CHUNK = 0x4e4f534a;
|
|
8
9
|
const BIN_CHUNK = 0x004e4942;
|
|
9
10
|
const IDENTITY = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1];
|
|
11
|
+
const MAX_ACCESSOR_VALUES = 30_000_000;
|
|
12
|
+
const TYPES = { SCALAR: 1, VEC2: 2, VEC3: 3, VEC4: 4, MAT2: 4, MAT3: 9, MAT4: 16 };
|
|
10
13
|
|
|
11
14
|
// componentType -> byte size, signedness and normalized divisor (glTF 2.0 table 3.1 / 5.1)
|
|
12
15
|
const COMPONENTS = {
|
|
@@ -18,6 +21,14 @@ const COMPONENTS = {
|
|
|
18
21
|
5126: { size: 4, signed: true, norm: 1, read: (b, o) => b.readFloatLE(o) }
|
|
19
22
|
};
|
|
20
23
|
|
|
24
|
+
const tryCatch = (fn) => {
|
|
25
|
+
try {
|
|
26
|
+
return [null, fn()];
|
|
27
|
+
} catch (error) {
|
|
28
|
+
return [error];
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
|
|
21
32
|
const mul = (a, b) => {
|
|
22
33
|
const o = new Array(16);
|
|
23
34
|
for (let c = 0; c < 4; c++) {
|
|
@@ -73,41 +84,99 @@ const round = (v) => Math.round(v * 1e4) / 1e4 + 0;
|
|
|
73
84
|
|
|
74
85
|
// buffer 0 of a GLB is the BIN chunk; other buffers are external files or data URIs
|
|
75
86
|
const bufferBytes = (g, bin, index) => {
|
|
76
|
-
const
|
|
77
|
-
if (
|
|
87
|
+
const buffer = g.buffers?.[index];
|
|
88
|
+
if (!buffer) return null;
|
|
89
|
+
const uri = buffer.uri;
|
|
90
|
+
if (uri === undefined) return index === 0 ? bin : null;
|
|
78
91
|
const base64 = /^data:[^,]*;base64,(.*)$/s.exec(uri);
|
|
79
92
|
return base64 ? Buffer.from(base64[1], 'base64') : null;
|
|
80
93
|
};
|
|
81
94
|
|
|
82
|
-
|
|
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) => {
|
|
95
|
+
const readAccessor = (g, bin, accessor, type = 'VEC3') => {
|
|
85
96
|
const acc = g.accessors?.[accessor];
|
|
86
|
-
if (!acc) return 'no-position-accessor';
|
|
87
|
-
if (acc.sparse) return '
|
|
88
|
-
if (acc.bufferView === undefined) return 'no-vertex-data';
|
|
97
|
+
if (!acc) return ['no-position-accessor'];
|
|
98
|
+
if (acc.bufferView === undefined && !acc.sparse) return ['no-vertex-data'];
|
|
89
99
|
const comp = COMPONENTS[acc.componentType];
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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';
|
|
100
|
+
const size = TYPES[acc.type];
|
|
101
|
+
if (!comp || !size || acc.type !== type || !Number.isInteger(acc.count) || acc.count < 1) {
|
|
102
|
+
return ['unsupported-accessor'];
|
|
103
|
+
}
|
|
104
|
+
if (acc.count * size > MAX_ACCESSOR_VALUES) return ['accessor-too-large'];
|
|
105
|
+
const data = new Float32Array(acc.count * size);
|
|
99
106
|
const scale = acc.normalized ? comp.norm : 1;
|
|
100
|
-
|
|
101
|
-
const
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
107
|
+
const read = (bytes, offset) => {
|
|
108
|
+
const value = comp.read(bytes, offset) / scale;
|
|
109
|
+
return acc.normalized && comp.signed && value < -1 ? -1 : value;
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
if (acc.bufferView !== undefined) {
|
|
113
|
+
const view = g.bufferViews?.[acc.bufferView];
|
|
114
|
+
if (!view || !Number.isInteger(view.byteLength)) return ['unsupported-accessor'];
|
|
115
|
+
if (view.extensions?.EXT_meshopt_compression) return ['meshopt-compressed'];
|
|
116
|
+
const bytes = bufferBytes(g, bin, view.buffer ?? 0);
|
|
117
|
+
if (!bytes) return ['external-buffer'];
|
|
118
|
+
const width = comp.size * size;
|
|
119
|
+
const stride = view.byteStride ?? width;
|
|
120
|
+
const base = (view.byteOffset ?? 0) + (acc.byteOffset ?? 0);
|
|
121
|
+
const used = (acc.byteOffset ?? 0) + (acc.count - 1) * stride + width;
|
|
122
|
+
if (stride < width || base < 0 || used > view.byteLength || base + (acc.count - 1) * stride + width > bytes.length) {
|
|
123
|
+
return ['truncated-buffer'];
|
|
124
|
+
}
|
|
125
|
+
for (let i = 0; i < acc.count; i++) {
|
|
126
|
+
for (let k = 0; k < size; k++) data[i * size + k] = read(bytes, base + i * stride + k * comp.size);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (acc.sparse) {
|
|
131
|
+
const sparse = acc.sparse;
|
|
132
|
+
const idx = sparse.indices;
|
|
133
|
+
const values = sparse.values;
|
|
134
|
+
const idxComp = idx && [5121, 5123, 5125].includes(idx.componentType) ? COMPONENTS[idx.componentType] : null;
|
|
135
|
+
const idxView = g.bufferViews?.[idx?.bufferView];
|
|
136
|
+
const valueView = g.bufferViews?.[values?.bufferView];
|
|
137
|
+
if (!Number.isInteger(sparse.count) || sparse.count < 1 || sparse.count > acc.count || !idxComp ||
|
|
138
|
+
!Number.isInteger(idxView?.byteLength) || !Number.isInteger(valueView?.byteLength)) {
|
|
139
|
+
return ['invalid-sparse-accessor'];
|
|
140
|
+
}
|
|
141
|
+
if (idxView.extensions?.EXT_meshopt_compression || valueView.extensions?.EXT_meshopt_compression) {
|
|
142
|
+
return ['meshopt-compressed'];
|
|
143
|
+
}
|
|
144
|
+
const idxBytes = bufferBytes(g, bin, idxView.buffer ?? 0);
|
|
145
|
+
const valueBytes = bufferBytes(g, bin, valueView.buffer ?? 0);
|
|
146
|
+
if (!idxBytes || !valueBytes) return ['external-buffer'];
|
|
147
|
+
const idxBase = (idxView.byteOffset ?? 0) + (idx.byteOffset ?? 0);
|
|
148
|
+
const valueBase = (valueView.byteOffset ?? 0) + (values.byteOffset ?? 0);
|
|
149
|
+
const valueWidth = comp.size * size;
|
|
150
|
+
const idxUsed = (idx.byteOffset ?? 0) + sparse.count * idxComp.size;
|
|
151
|
+
const valueUsed = (values.byteOffset ?? 0) + sparse.count * valueWidth;
|
|
152
|
+
if (idxBase < 0 || valueBase < 0 || idxUsed > idxView.byteLength || valueUsed > valueView.byteLength ||
|
|
153
|
+
idxBase + sparse.count * idxComp.size > idxBytes.length || valueBase + sparse.count * valueWidth > valueBytes.length) {
|
|
154
|
+
return ['truncated-buffer'];
|
|
155
|
+
}
|
|
156
|
+
let prev = -1;
|
|
157
|
+
for (let i = 0; i < sparse.count; i++) {
|
|
158
|
+
const index = idxComp.read(idxBytes, idxBase + i * idxComp.size);
|
|
159
|
+
if (index <= prev || index >= acc.count) return ['invalid-sparse-accessor'];
|
|
160
|
+
prev = index;
|
|
161
|
+
for (let k = 0; k < size; k++) {
|
|
162
|
+
data[index * size + k] = read(valueBytes, valueBase + i * valueWidth + k * comp.size);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return [null, data];
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
const accumulate = (data, matrix, min, max, morphs = []) => {
|
|
171
|
+
for (let i = 0; i < data.length; i += 3) {
|
|
172
|
+
let x = data[i];
|
|
173
|
+
let y = data[i + 1];
|
|
174
|
+
let z = data[i + 2];
|
|
175
|
+
for (const [weight, delta] of morphs) {
|
|
176
|
+
x += delta[i] * weight;
|
|
177
|
+
y += delta[i + 1] * weight;
|
|
178
|
+
z += delta[i + 2] * weight;
|
|
109
179
|
}
|
|
110
|
-
// inlined to avoid allocating a vector per vertex on million-vertex meshes
|
|
111
180
|
const px = matrix[0] * x + matrix[4] * y + matrix[8] * z + matrix[12];
|
|
112
181
|
const py = matrix[1] * x + matrix[5] * y + matrix[9] * z + matrix[13];
|
|
113
182
|
const pz = matrix[2] * x + matrix[6] * y + matrix[10] * z + matrix[14];
|
|
@@ -118,13 +187,12 @@ const accumulate = (g, bin, accessor, matrix, min, max) => {
|
|
|
118
187
|
if (py > max[1]) max[1] = py;
|
|
119
188
|
if (pz > max[2]) max[2] = pz;
|
|
120
189
|
}
|
|
121
|
-
return null;
|
|
122
190
|
};
|
|
123
191
|
|
|
124
192
|
// loose fallback: bound the 8 corners of the accessor's local box. over-estimates a mesh that does
|
|
125
193
|
// not fill its box when the node rotation is not a multiple of 90 degrees.
|
|
126
194
|
const accumulateCorners = (acc, matrix, min, max) => {
|
|
127
|
-
if (!acc?.min || !acc?.max) return;
|
|
195
|
+
if (!acc?.min || !acc?.max) return false;
|
|
128
196
|
for (let c = 0; c < 8; c++) {
|
|
129
197
|
const point = xform(matrix, [
|
|
130
198
|
c & 1 ? acc.max[0] : acc.min[0],
|
|
@@ -136,35 +204,55 @@ const accumulateCorners = (acc, matrix, min, max) => {
|
|
|
136
204
|
if (point[k] > max[k]) max[k] = point[k];
|
|
137
205
|
}
|
|
138
206
|
}
|
|
207
|
+
return true;
|
|
139
208
|
};
|
|
140
209
|
|
|
141
210
|
export const inspectGlb = (buf) => {
|
|
142
211
|
if (buf.length < 20 || buf.readUInt32LE(0) !== GLB_MAGIC || buf.readUInt32LE(16) !== JSON_CHUNK) {
|
|
143
212
|
throw new Error('not a GLB (or JSON chunk missing)');
|
|
144
213
|
}
|
|
214
|
+
if (buf.readUInt32LE(4) !== 2) throw new Error('unsupported GLB version');
|
|
215
|
+
if (buf.readUInt32LE(8) !== buf.length) throw new Error('invalid GLB length');
|
|
145
216
|
const jsonLength = buf.readUInt32LE(12);
|
|
217
|
+
if (20 + jsonLength > buf.length) throw new Error('invalid JSON chunk length');
|
|
146
218
|
const g = JSON.parse(buf.toString('utf8', 20, 20 + jsonLength));
|
|
147
219
|
const binAt = 20 + jsonLength;
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
220
|
+
let bin = null;
|
|
221
|
+
if (binAt < buf.length) {
|
|
222
|
+
if (binAt + 8 > buf.length || buf.readUInt32LE(binAt + 4) !== BIN_CHUNK) {
|
|
223
|
+
throw new Error('invalid BIN chunk');
|
|
224
|
+
}
|
|
225
|
+
const length = buf.readUInt32LE(binAt);
|
|
226
|
+
if (binAt + 8 + length !== buf.length) throw new Error('invalid BIN chunk length');
|
|
227
|
+
bin = buf.subarray(binAt + 8);
|
|
228
|
+
}
|
|
152
229
|
const nodes = g.nodes ?? [];
|
|
153
230
|
const nodeName = (i) => nodes[i]?.name ?? `node_${i}`;
|
|
154
231
|
const parents = new Array(nodes.length).fill(-1);
|
|
155
232
|
for (let i = 0; i < nodes.length; i++) {
|
|
156
|
-
for (const child of nodes[i].children ?? [])
|
|
233
|
+
for (const child of nodes[i].children ?? []) {
|
|
234
|
+
if (!Number.isInteger(child) || !nodes[child] || parents[child] !== -1) throw new Error('invalid node graph');
|
|
235
|
+
parents[child] = i;
|
|
236
|
+
}
|
|
157
237
|
}
|
|
158
238
|
const nodePath = (i) => {
|
|
159
239
|
const names = [];
|
|
160
|
-
|
|
240
|
+
const seen = new Set();
|
|
241
|
+
for (; i >= 0; i = parents[i]) {
|
|
242
|
+
if (seen.has(i)) throw new Error('invalid node graph');
|
|
243
|
+
seen.add(i);
|
|
244
|
+
names.unshift(nodeName(i));
|
|
245
|
+
}
|
|
161
246
|
return names.join('/');
|
|
162
247
|
};
|
|
163
248
|
const children = new Set(nodes.flatMap((n) => n.children ?? []));
|
|
164
249
|
const roots = g.scenes?.[g.scene ?? 0]?.nodes ?? nodes.map((_, i) => i).filter((i) => !children.has(i));
|
|
165
250
|
|
|
166
251
|
const world = new Array(nodes.length);
|
|
252
|
+
const seen = new Set();
|
|
167
253
|
const walk = (i, parent) => {
|
|
254
|
+
if (!Number.isInteger(i) || !nodes[i] || seen.has(i)) throw new Error('invalid node graph');
|
|
255
|
+
seen.add(i);
|
|
168
256
|
const n = nodes[i];
|
|
169
257
|
const local = n.matrix ?? compose(n.translation ?? [0, 0, 0], n.rotation ?? [0, 0, 0, 1], n.scale ?? [1, 1, 1]);
|
|
170
258
|
world[i] = mul(parent, local);
|
|
@@ -176,28 +264,67 @@ export const inspectGlb = (buf) => {
|
|
|
176
264
|
const max = [-Infinity, -Infinity, -Infinity];
|
|
177
265
|
const notes = new Set();
|
|
178
266
|
let meshNodes = 0;
|
|
267
|
+
let morphed = false;
|
|
268
|
+
let fallback = false;
|
|
269
|
+
let incomplete = false;
|
|
179
270
|
for (let i = 0; i < nodes.length; i++) {
|
|
180
271
|
if (nodes[i].mesh === undefined || !world[i]) continue;
|
|
181
272
|
meshNodes++;
|
|
182
273
|
// glTF 2.0: "the transform of the skinned mesh node MUST be ignored" - joints drive the pose
|
|
183
274
|
const matrix = nodes[i].skin === undefined ? world[i] : IDENTITY;
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
275
|
+
const mesh = g.meshes?.[nodes[i].mesh];
|
|
276
|
+
for (const prim of mesh?.primitives ?? []) {
|
|
277
|
+
const [error, data] = prim.extensions?.KHR_draco_mesh_compression
|
|
278
|
+
? ['draco-compressed']
|
|
279
|
+
: readAccessor(g, bin, prim.attributes?.POSITION);
|
|
280
|
+
if (error) {
|
|
281
|
+
notes.add(error);
|
|
282
|
+
if (error === 'meshopt-compressed' || !accumulateCorners(g.accessors?.[prim.attributes?.POSITION], matrix, min, max)) {
|
|
283
|
+
incomplete = true;
|
|
284
|
+
} else {
|
|
285
|
+
fallback = true;
|
|
286
|
+
}
|
|
287
|
+
continue;
|
|
288
|
+
}
|
|
289
|
+
const morphs = [];
|
|
290
|
+
if (prim.targets?.length) {
|
|
291
|
+
morphed = true;
|
|
292
|
+
const weights = nodes[i].weights ?? mesh.weights ?? [];
|
|
293
|
+
for (let k = 0; k < prim.targets.length; k++) {
|
|
294
|
+
const weight = weights[k] ?? 0;
|
|
295
|
+
if (!Number.isFinite(weight)) {
|
|
296
|
+
notes.add('invalid-morph-weight');
|
|
297
|
+
incomplete = true;
|
|
298
|
+
continue;
|
|
299
|
+
}
|
|
300
|
+
const accessor = prim.targets[k].POSITION;
|
|
301
|
+
if (!weight || accessor === undefined) continue;
|
|
302
|
+
const [morphError, delta] = readAccessor(g, bin, accessor);
|
|
303
|
+
if (morphError || delta.length !== data.length) {
|
|
304
|
+
notes.add(`morph-${morphError ?? 'accessor-size'}`);
|
|
305
|
+
incomplete = true;
|
|
306
|
+
continue;
|
|
307
|
+
}
|
|
308
|
+
morphs.push([weight, delta]);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
accumulate(data, matrix, min, max, morphs);
|
|
191
312
|
}
|
|
192
313
|
}
|
|
193
314
|
|
|
194
315
|
const ok = min[0] !== Infinity;
|
|
316
|
+
const morphAnimated = (g.animations ?? []).some((a) =>
|
|
317
|
+
(a.channels ?? []).some((c) => c.target?.path === 'weights')
|
|
318
|
+
);
|
|
319
|
+
const skinned = !!g.skins?.length;
|
|
195
320
|
return {
|
|
196
321
|
aabb: ok ? { min: min.map(round), max: max.map(round) } : null,
|
|
197
322
|
dims: ok ? max.map((v, i) => round(v - min[i])) : null,
|
|
198
323
|
center: ok ? max.map((v, i) => round((v + min[i]) / 2)) : null,
|
|
199
324
|
groundOffset: ok ? round(-min[1]) : null,
|
|
200
|
-
boundsSource: ok ? (
|
|
325
|
+
boundsSource: ok ? (incomplete ? 'incomplete' : fallback ? 'accessor-minmax' : 'vertices') : null,
|
|
326
|
+
boundsPose: skinned ? 'bind' : morphed ? 'default-morph' : 'static',
|
|
327
|
+
requiresRuntimeCheck: skinned || morphAnimated || notes.size > 0,
|
|
201
328
|
...(notes.size ? { boundsNotes: [...notes].sort() } : {}),
|
|
202
329
|
nodes: nodes.length,
|
|
203
330
|
nodePaths: nodes.map((_, i) => nodePath(i)).sort(),
|
|
@@ -217,11 +344,15 @@ export const inspectGlb = (buf) => {
|
|
|
217
344
|
)
|
|
218
345
|
)
|
|
219
346
|
].sort(),
|
|
220
|
-
|
|
347
|
+
morphed,
|
|
348
|
+
morphAnimated,
|
|
349
|
+
skinned
|
|
221
350
|
};
|
|
222
351
|
};
|
|
223
352
|
|
|
224
|
-
|
|
353
|
+
const main = import.meta.main ?? (process.argv[1] &&
|
|
354
|
+
fs.realpathSync(fileURLToPath(import.meta.url)) === fs.realpathSync(process.argv[1]));
|
|
355
|
+
if (main) {
|
|
225
356
|
const files = process.argv.slice(2);
|
|
226
357
|
if (!files.length) {
|
|
227
358
|
console.error('usage: node inspect.mjs <file.glb> [more.glb ...]');
|
|
@@ -229,11 +360,9 @@ if (import.meta.main ?? import.meta.filename === process.argv[1]) {
|
|
|
229
360
|
}
|
|
230
361
|
// report per-file failures so one unreadable file cannot discard a whole glob
|
|
231
362
|
const out = files.map((file) => {
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
} catch (e) {
|
|
235
|
-
return { file, error: e.message };
|
|
236
|
-
}
|
|
363
|
+
const [error, result] = tryCatch(() => inspectGlb(fs.readFileSync(file)));
|
|
364
|
+
return error ? { file, error: error instanceof Error ? error.message : String(error) } : { file, ...result };
|
|
237
365
|
});
|
|
366
|
+
if (out.some((result) => result.error)) process.exitCode = 1;
|
|
238
367
|
console.log(JSON.stringify(out.length === 1 ? out[0] : out, null, 2));
|
|
239
368
|
}
|
|
@@ -8,9 +8,9 @@ Expose an Engine script as an ES module and register it as an asset:
|
|
|
8
8
|
<pc-scene>
|
|
9
9
|
<pc-entity name="Camera">
|
|
10
10
|
<pc-camera></pc-camera>
|
|
11
|
-
<pc-
|
|
12
|
-
<pc-script name="cameraControls" move-speed="4"></pc-script>
|
|
13
|
-
</pc-
|
|
11
|
+
<pc-script>
|
|
12
|
+
<pc-script-instance name="cameraControls" move-speed="4"></pc-script-instance>
|
|
13
|
+
</pc-script>
|
|
14
14
|
</pc-entity>
|
|
15
15
|
</pc-scene>
|
|
16
16
|
</pc-app>
|
|
@@ -39,9 +39,9 @@ so `document.querySelector` reaches them and `setAttribute` changes them.
|
|
|
39
39
|
|
|
40
40
|
`src/main.ts` covers the two things HTML alone cannot do:
|
|
41
41
|
|
|
42
|
-
- **Attaching engine scripts.** `<pc-script name="...">` resolves scripts that `<pc-asset>`
|
|
43
|
-
runtime. Scripts imported through the bundler — like `CameraControls` and `Grid` here — are
|
|
44
|
-
through the entity instead, reached via `whenReady()` and the element's `.entity` property.
|
|
42
|
+
- **Attaching engine scripts.** `<pc-script-instance name="...">` resolves scripts that `<pc-asset>`
|
|
43
|
+
fetched at runtime. Scripts imported through the bundler — like `CameraControls` and `Grid` here — are
|
|
44
|
+
attached through the entity instead, reached via `whenReady()` and the element's `.entity` property.
|
|
45
45
|
- **Responding to pointer input.** `<pc-app>` picks whatever is under the pointer and dispatches an
|
|
46
46
|
ordinary `PointerEvent` on the `<pc-entity>` it hit, so `addEventListener('pointerup', ...)` works on a
|
|
47
47
|
3D entity exactly as it would on a `<button>`. Events are only generated for entities that have a
|
|
@@ -14,12 +14,12 @@
|
|
|
14
14
|
<pc-material id="green-material" diffuse="#2e6638" emissive="#09140b" gloss="0.2"></pc-material>
|
|
15
15
|
<pc-material id="wood-material" diffuse="#7a451a" emissive="#180e05" gloss="0.2"></pc-material>
|
|
16
16
|
<pc-material id="metal-material" diffuse="#475461" emissive="#0e1113" gloss="0.5"></pc-material>
|
|
17
|
-
<pc-
|
|
17
|
+
<pc-wasm
|
|
18
18
|
name="Ammo"
|
|
19
19
|
glue="/ammo/ammo.wasm.js"
|
|
20
20
|
wasm="/ammo/ammo.wasm.wasm"
|
|
21
21
|
fallback="/ammo/ammo.js"
|
|
22
|
-
></pc-
|
|
22
|
+
></pc-wasm>
|
|
23
23
|
<pc-scene>
|
|
24
24
|
<pc-entity name="light" rotation="50 30 0"
|
|
25
25
|
><pc-light
|
|
@@ -36,121 +36,121 @@
|
|
|
36
36
|
<pc-entity name="floor" position="0 -0.1 0" scale="18 0.2 18"
|
|
37
37
|
><pc-render type="box" material="floor-material"></pc-render
|
|
38
38
|
><pc-collision type="box" half-extents="9 0.1 9"></pc-collision
|
|
39
|
-
><pc-
|
|
39
|
+
><pc-rigid-body type="static"></pc-rigid-body
|
|
40
40
|
></pc-entity>
|
|
41
41
|
<pc-entity name="back-wall" position="0 1.5 -9" scale="18 3 0.3"
|
|
42
42
|
><pc-render type="box" material="wall-material"></pc-render
|
|
43
43
|
><pc-collision type="box" half-extents="9 1.5 0.15"></pc-collision
|
|
44
|
-
><pc-
|
|
44
|
+
><pc-rigid-body type="static"></pc-rigid-body
|
|
45
45
|
></pc-entity>
|
|
46
46
|
<pc-entity name="front-wall" position="0 1.5 9" scale="18 3 0.3"
|
|
47
47
|
><pc-render type="box" material="wall-material"></pc-render
|
|
48
48
|
><pc-collision type="box" half-extents="9 1.5 0.15"></pc-collision
|
|
49
|
-
><pc-
|
|
49
|
+
><pc-rigid-body type="static"></pc-rigid-body
|
|
50
50
|
></pc-entity>
|
|
51
51
|
<pc-entity name="left-wall" position="-9 1.5 0" scale="0.3 3 18"
|
|
52
52
|
><pc-render type="box" material="wall-material"></pc-render
|
|
53
53
|
><pc-collision type="box" half-extents="0.15 1.5 9"></pc-collision
|
|
54
|
-
><pc-
|
|
54
|
+
><pc-rigid-body type="static"></pc-rigid-body
|
|
55
55
|
></pc-entity>
|
|
56
56
|
<pc-entity name="right-wall" position="9 1.5 0" scale="0.3 3 18"
|
|
57
57
|
><pc-render type="box" material="wall-material"></pc-render
|
|
58
58
|
><pc-collision type="box" half-extents="0.15 1.5 9"></pc-collision
|
|
59
|
-
><pc-
|
|
59
|
+
><pc-rigid-body type="static"></pc-rigid-body
|
|
60
60
|
></pc-entity>
|
|
61
61
|
<pc-entity name="blue-crate" position="-2 0.75 -3" scale="1.5 1.5 1.5"
|
|
62
62
|
><pc-render type="box" material="blue-material"></pc-render
|
|
63
63
|
><pc-collision type="box" half-extents="0.75 0.75 0.75"></pc-collision
|
|
64
|
-
><pc-
|
|
64
|
+
><pc-rigid-body type="static"></pc-rigid-body
|
|
65
65
|
></pc-entity>
|
|
66
66
|
<pc-entity name="green-crate" position="3 0.5 1" scale="2.5 1 1"
|
|
67
67
|
><pc-render type="box" material="green-material"></pc-render
|
|
68
68
|
><pc-collision type="box" half-extents="1.25 0.5 0.5"></pc-collision
|
|
69
|
-
><pc-
|
|
69
|
+
><pc-rigid-body type="static"></pc-rigid-body
|
|
70
70
|
></pc-entity>
|
|
71
71
|
<pc-entity name="cargo-left-0" position="-5.5 0.75 -5.5" scale="2 1.5 2"
|
|
72
72
|
><pc-render type="box" material="wood-material"></pc-render
|
|
73
73
|
><pc-collision type="box" half-extents="1 0.75 1"></pc-collision
|
|
74
|
-
><pc-
|
|
74
|
+
><pc-rigid-body type="static"></pc-rigid-body
|
|
75
75
|
></pc-entity>
|
|
76
76
|
<pc-entity name="cargo-left-1" position="-5.5 2 -5.5" scale="1.5 1 1.5"
|
|
77
77
|
><pc-render type="box" material="blue-material"></pc-render
|
|
78
78
|
><pc-collision type="box" half-extents="0.75 0.5 0.75"></pc-collision
|
|
79
|
-
><pc-
|
|
79
|
+
><pc-rigid-body type="static"></pc-rigid-body
|
|
80
80
|
></pc-entity>
|
|
81
81
|
<pc-entity name="cargo-right-0" position="5.3 0.6 -4.8" scale="2.4 1.2 2"
|
|
82
82
|
><pc-render type="box" material="green-material"></pc-render
|
|
83
83
|
><pc-collision type="box" half-extents="1.2 0.6 1"></pc-collision
|
|
84
|
-
><pc-
|
|
84
|
+
><pc-rigid-body type="static"></pc-rigid-body
|
|
85
85
|
></pc-entity>
|
|
86
86
|
<pc-entity name="cargo-right-1" position="5.3 1.7 -4.8" scale="1.5 1 1.5"
|
|
87
87
|
><pc-render type="box" material="wood-material"></pc-render
|
|
88
88
|
><pc-collision type="box" half-extents="0.75 0.5 0.75"></pc-collision
|
|
89
|
-
><pc-
|
|
89
|
+
><pc-rigid-body type="static"></pc-rigid-body
|
|
90
90
|
></pc-entity>
|
|
91
91
|
<pc-entity name="cargo-center" position="0 0.6 -6.5" scale="2.2 1.2 1.8"
|
|
92
92
|
><pc-render type="box" material="blue-material"></pc-render
|
|
93
93
|
><pc-collision type="box" half-extents="1.1 0.6 0.9"></pc-collision
|
|
94
|
-
><pc-
|
|
94
|
+
><pc-rigid-body type="static"></pc-rigid-body
|
|
95
95
|
></pc-entity>
|
|
96
96
|
<pc-entity name="shelf-left-post-0" position="-6.8 1.4 -3.5" scale="0.25 2.8 0.25"
|
|
97
97
|
><pc-render type="box" material="metal-material"></pc-render
|
|
98
98
|
><pc-collision type="box" half-extents="0.125 1.4 0.125"></pc-collision
|
|
99
|
-
><pc-
|
|
99
|
+
><pc-rigid-body type="static"></pc-rigid-body
|
|
100
100
|
></pc-entity>
|
|
101
101
|
<pc-entity name="shelf-left-post-1" position="-6.8 1.4 0" scale="0.25 2.8 0.25"
|
|
102
102
|
><pc-render type="box" material="metal-material"></pc-render
|
|
103
103
|
><pc-collision type="box" half-extents="0.125 1.4 0.125"></pc-collision
|
|
104
|
-
><pc-
|
|
104
|
+
><pc-rigid-body type="static"></pc-rigid-body
|
|
105
105
|
></pc-entity>
|
|
106
106
|
<pc-entity name="shelf-left-post-2" position="-6.8 1.4 3.5" scale="0.25 2.8 0.25"
|
|
107
107
|
><pc-render type="box" material="metal-material"></pc-render
|
|
108
108
|
><pc-collision type="box" half-extents="0.125 1.4 0.125"></pc-collision
|
|
109
|
-
><pc-
|
|
109
|
+
><pc-rigid-body type="static"></pc-rigid-body
|
|
110
110
|
></pc-entity>
|
|
111
111
|
<pc-entity name="shelf-right-post-0" position="6.8 1.4 -3.5" scale="0.25 2.8 0.25"
|
|
112
112
|
><pc-render type="box" material="metal-material"></pc-render
|
|
113
113
|
><pc-collision type="box" half-extents="0.125 1.4 0.125"></pc-collision
|
|
114
|
-
><pc-
|
|
114
|
+
><pc-rigid-body type="static"></pc-rigid-body
|
|
115
115
|
></pc-entity>
|
|
116
116
|
<pc-entity name="shelf-right-post-1" position="6.8 1.4 0" scale="0.25 2.8 0.25"
|
|
117
117
|
><pc-render type="box" material="metal-material"></pc-render
|
|
118
118
|
><pc-collision type="box" half-extents="0.125 1.4 0.125"></pc-collision
|
|
119
|
-
><pc-
|
|
119
|
+
><pc-rigid-body type="static"></pc-rigid-body
|
|
120
120
|
></pc-entity>
|
|
121
121
|
<pc-entity name="shelf-right-post-2" position="6.8 1.4 3.5" scale="0.25 2.8 0.25"
|
|
122
122
|
><pc-render type="box" material="metal-material"></pc-render
|
|
123
123
|
><pc-collision type="box" half-extents="0.125 1.4 0.125"></pc-collision
|
|
124
|
-
><pc-
|
|
124
|
+
><pc-rigid-body type="static"></pc-rigid-body
|
|
125
125
|
></pc-entity>
|
|
126
126
|
<pc-entity name="shelf-left-low" position="-6.8 0.65 0" scale="1.1 0.18 7.5"
|
|
127
127
|
><pc-render type="box" material="metal-material"></pc-render
|
|
128
128
|
><pc-collision type="box" half-extents="0.55 0.09 3.75"></pc-collision
|
|
129
|
-
><pc-
|
|
129
|
+
><pc-rigid-body type="static"></pc-rigid-body
|
|
130
130
|
></pc-entity>
|
|
131
131
|
<pc-entity name="shelf-left-high" position="-6.8 2.1 0" scale="1.1 0.18 7.5"
|
|
132
132
|
><pc-render type="box" material="metal-material"></pc-render
|
|
133
133
|
><pc-collision type="box" half-extents="0.55 0.09 3.75"></pc-collision
|
|
134
|
-
><pc-
|
|
134
|
+
><pc-rigid-body type="static"></pc-rigid-body
|
|
135
135
|
></pc-entity>
|
|
136
136
|
<pc-entity name="shelf-right-low" position="6.8 0.65 0" scale="1.1 0.18 7.5"
|
|
137
137
|
><pc-render type="box" material="metal-material"></pc-render
|
|
138
138
|
><pc-collision type="box" half-extents="0.55 0.09 3.75"></pc-collision
|
|
139
|
-
><pc-
|
|
139
|
+
><pc-rigid-body type="static"></pc-rigid-body
|
|
140
140
|
></pc-entity>
|
|
141
141
|
<pc-entity name="shelf-right-high" position="6.8 2.1 0" scale="1.1 0.18 7.5"
|
|
142
142
|
><pc-render type="box" material="metal-material"></pc-render
|
|
143
143
|
><pc-collision type="box" half-extents="0.55 0.09 3.75"></pc-collision
|
|
144
|
-
><pc-
|
|
144
|
+
><pc-rigid-body type="static"></pc-rigid-body
|
|
145
145
|
></pc-entity>
|
|
146
146
|
<pc-entity name="pallet" position="-2.5 0.15 2.5" scale="3 0.3 2"
|
|
147
147
|
><pc-render type="box" material="wood-material"></pc-render
|
|
148
148
|
><pc-collision type="box" half-extents="1.5 0.15 1"></pc-collision
|
|
149
|
-
><pc-
|
|
149
|
+
><pc-rigid-body type="static"></pc-rigid-body
|
|
150
150
|
></pc-entity>
|
|
151
151
|
<pc-entity name="player" position="0 1 5">
|
|
152
152
|
<pc-collision type="capsule" radius="0.45" height="1.8"></pc-collision>
|
|
153
|
-
<pc-
|
|
153
|
+
<pc-rigid-body type="dynamic" mass="80" angular-factor="0 0 0"></pc-rigid-body>
|
|
154
154
|
<pc-entity name="camera" position="0 0.65 0"
|
|
155
155
|
><pc-camera clear-color="#61a3db" fov="80"></pc-camera
|
|
156
156
|
></pc-entity>
|
|
@@ -6,7 +6,7 @@ import './starter.css';
|
|
|
6
6
|
|
|
7
7
|
await Promise.all([
|
|
8
8
|
whenReady('pc-entity[name="player"] > pc-collision'),
|
|
9
|
-
whenReady('pc-entity[name="player"] > pc-
|
|
9
|
+
whenReady('pc-entity[name="player"] > pc-rigid-body')
|
|
10
10
|
]);
|
|
11
11
|
const [player, camera] = await Promise.all([
|
|
12
12
|
whenReady<EntityElement>('pc-entity[name="player"]'),
|
|
@@ -47,7 +47,7 @@ if (skyboxLayer) {
|
|
|
47
47
|
}
|
|
48
48
|
|
|
49
49
|
// Engine scripts are resolved by the bundler, so they are attached through the entity rather than
|
|
50
|
-
// with <pc-script name="...">, which only resolves scripts fetched at runtime by <pc-asset>
|
|
50
|
+
// with <pc-script-instance name="...">, which only resolves scripts fetched at runtime by <pc-asset>
|
|
51
51
|
const camera = cameraComponent.closestEntity;
|
|
52
52
|
camera?.entity?.addComponent('script');
|
|
53
53
|
camera?.entity?.script?.create(CameraControls, { properties: { sceneSize: 3 } });
|
|
@@ -14,7 +14,7 @@ const [cameraComponent, sky] = await Promise.all([
|
|
|
14
14
|
]);
|
|
15
15
|
|
|
16
16
|
// Engine scripts are resolved by the bundler, so they are attached through the entity rather than
|
|
17
|
-
// with <pc-script name="...">, which only resolves scripts fetched at runtime by <pc-asset>
|
|
17
|
+
// with <pc-script-instance name="...">, which only resolves scripts fetched at runtime by <pc-asset>
|
|
18
18
|
const camera = cameraComponent.closestEntity;
|
|
19
19
|
camera?.entity?.addComponent('script');
|
|
20
20
|
camera?.entity?.script?.create(CameraControls, { properties: { sceneSize: 2.5 } });
|
|
@@ -8,12 +8,12 @@
|
|
|
8
8
|
</head>
|
|
9
9
|
<body>
|
|
10
10
|
<pc-app>
|
|
11
|
-
<pc-
|
|
11
|
+
<pc-wasm
|
|
12
12
|
name="Ammo"
|
|
13
13
|
glue="/ammo/ammo.wasm.js"
|
|
14
14
|
wasm="/ammo/ammo.wasm.wasm"
|
|
15
15
|
fallback="/ammo/ammo.js"
|
|
16
|
-
></pc-
|
|
16
|
+
></pc-wasm>
|
|
17
17
|
<pc-material id="floor-material" diffuse="#2e333d" gloss="0.2"></pc-material>
|
|
18
18
|
<pc-material id="cyan-material" diffuse="#29ade6" gloss="0.45"></pc-material>
|
|
19
19
|
<pc-material id="coral-material" diffuse="#f25738" gloss="0.45"></pc-material>
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
<pc-entity name="floor" position="0 -0.1 0" scale="10 0.2 10"
|
|
35
35
|
><pc-render type="box" material="floor-material"></pc-render
|
|
36
36
|
><pc-collision type="box" half-extents="5 0.1 5"></pc-collision
|
|
37
|
-
><pc-
|
|
37
|
+
><pc-rigid-body type="static"></pc-rigid-body
|
|
38
38
|
></pc-entity>
|
|
39
39
|
<pc-entity id="bodies"></pc-entity>
|
|
40
40
|
</pc-scene>
|
|
@@ -18,7 +18,7 @@ const spawn = () => {
|
|
|
18
18
|
const type = TYPES[i % TYPES.length];
|
|
19
19
|
bodies.insertAdjacentHTML(
|
|
20
20
|
'beforeend',
|
|
21
|
-
`<pc-entity data-body position="${((i % 4) - 1.5) * 1.1} ${0.7 + Math.floor(i / 4) * 1.1} ${(Math.floor(i / 4) - 1) * 1.4}" rotation="${i * 13} ${i * 29} 0" scale="${SCALES[i % SCALES.length]}"><pc-render type="${type}" material="${MATERIALS[i % MATERIALS.length]}"></pc-render><pc-collision type="${type}"></pc-collision><pc-
|
|
21
|
+
`<pc-entity data-body position="${((i % 4) - 1.5) * 1.1} ${0.7 + Math.floor(i / 4) * 1.1} ${(Math.floor(i / 4) - 1) * 1.4}" rotation="${i * 13} ${i * 29} 0" scale="${SCALES[i % SCALES.length]}"><pc-render type="${type}" material="${MATERIALS[i % MATERIALS.length]}"></pc-render><pc-collision type="${type}"></pc-collision><pc-rigid-body type="dynamic" mass="1" restitution="0.35"></pc-rigid-body></pc-entity>`
|
|
22
22
|
);
|
|
23
23
|
};
|
|
24
24
|
const reset = () => {
|
|
@@ -58,8 +58,10 @@ ground.addComponent('render', {
|
|
|
58
58
|
receiveShadows: true
|
|
59
59
|
});
|
|
60
60
|
|
|
61
|
+
// The element's entity is a host whose transform composes with the GLB's authored root rotation
|
|
62
|
+
// (a -90° X up-axis fix), so only the yaw is set here
|
|
61
63
|
const entity = model.entity!;
|
|
62
|
-
entity.setLocalEulerAngles(
|
|
64
|
+
entity.setLocalEulerAngles(0, -25, 0);
|
|
63
65
|
entity.root.syncHierarchy();
|
|
64
66
|
const meshes = (entity.findComponents('render') as RenderComponent[]).flatMap((render) => render.meshInstances);
|
|
65
67
|
const bounds = meshes[0].aabb.clone();
|
|
@@ -14,7 +14,7 @@ class Rotate extends Script {
|
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
// Engine scripts are resolved by the bundler, so they are attached through the entity rather than
|
|
17
|
-
// with <pc-script name="...">, which only resolves scripts fetched at runtime by <pc-asset>
|
|
17
|
+
// with <pc-script-instance name="...">, which only resolves scripts fetched at runtime by <pc-asset>
|
|
18
18
|
const cube = await whenReady<EntityElement>('pc-entity[name="cube"]');
|
|
19
19
|
|
|
20
20
|
cube.entity?.addComponent('script');
|
|
@@ -9,7 +9,7 @@ import './style.css';
|
|
|
9
9
|
const cameraComponent = await whenReady('pc-camera');
|
|
10
10
|
|
|
11
11
|
// Engine scripts are resolved by the bundler, so they are attached through the entity rather than
|
|
12
|
-
// with <pc-script name="...">, which only resolves scripts fetched at runtime by <pc-asset>
|
|
12
|
+
// with <pc-script-instance name="...">, which only resolves scripts fetched at runtime by <pc-asset>
|
|
13
13
|
const camera = cameraComponent.closestEntity;
|
|
14
14
|
camera?.entity?.addComponent('script');
|
|
15
15
|
camera?.entity?.script?.create(CameraControls, { properties: { sceneSize: 2 } });
|
|
@@ -16,12 +16,12 @@
|
|
|
16
16
|
<pc-material id="shirt-material" diffuse="#268bd2"></pc-material>
|
|
17
17
|
<pc-material id="skin-material" diffuse="#c78759"></pc-material>
|
|
18
18
|
<pc-material id="pants-material" diffuse="#2e4a85"></pc-material>
|
|
19
|
-
<pc-
|
|
19
|
+
<pc-wasm
|
|
20
20
|
name="Ammo"
|
|
21
21
|
glue="/ammo/ammo.wasm.js"
|
|
22
22
|
wasm="/ammo/ammo.wasm.wasm"
|
|
23
23
|
fallback="/ammo/ammo.js"
|
|
24
|
-
></pc-
|
|
24
|
+
></pc-wasm>
|
|
25
25
|
<pc-scene>
|
|
26
26
|
<pc-entity name="camera" position="0 3 6"><pc-camera clear-color="#7ab8e6"></pc-camera></pc-entity>
|
|
27
27
|
<pc-entity name="light" rotation="45 30 0"
|
|
@@ -36,32 +36,32 @@
|
|
|
36
36
|
<pc-entity name="ground" position="0 -0.1 0" scale="20 0.2 20"
|
|
37
37
|
><pc-render type="box" material="ground-material"></pc-render
|
|
38
38
|
><pc-collision type="box" half-extents="10 0.1 10"></pc-collision
|
|
39
|
-
><pc-
|
|
39
|
+
><pc-rigid-body type="static"></pc-rigid-body
|
|
40
40
|
></pc-entity>
|
|
41
41
|
<pc-entity name="block-0" position="-4.5 0.5 -3.5" scale="2.4 1 2.4"
|
|
42
42
|
><pc-render type="box" material="block-material"></pc-render
|
|
43
43
|
><pc-collision type="box" half-extents="1.2 0.5 1.2"></pc-collision
|
|
44
|
-
><pc-
|
|
44
|
+
><pc-rigid-body type="static"></pc-rigid-body
|
|
45
45
|
></pc-entity>
|
|
46
46
|
<pc-entity name="block-1" position="4.2 0.5 -2.8" rotation="0 25 0" scale="2.4 1 2.4"
|
|
47
47
|
><pc-render type="box" material="block-material"></pc-render
|
|
48
48
|
><pc-collision type="box" half-extents="1.2 0.5 1.2"></pc-collision
|
|
49
|
-
><pc-
|
|
49
|
+
><pc-rigid-body type="static"></pc-rigid-body
|
|
50
50
|
></pc-entity>
|
|
51
51
|
<pc-entity name="block-2" position="-3.7 0.5 3.2" rotation="0 50 0" scale="2.4 1 2.4"
|
|
52
52
|
><pc-render type="box" material="block-material"></pc-render
|
|
53
53
|
><pc-collision type="box" half-extents="1.2 0.5 1.2"></pc-collision
|
|
54
|
-
><pc-
|
|
54
|
+
><pc-rigid-body type="static"></pc-rigid-body
|
|
55
55
|
></pc-entity>
|
|
56
56
|
<pc-entity name="block-3" position="3.8 0.5 3.6" rotation="0 75 0" scale="2.4 1 2.4"
|
|
57
57
|
><pc-render type="box" material="block-material"></pc-render
|
|
58
58
|
><pc-collision type="box" half-extents="1.2 0.5 1.2"></pc-collision
|
|
59
|
-
><pc-
|
|
59
|
+
><pc-rigid-body type="static"></pc-rigid-body
|
|
60
60
|
></pc-entity>
|
|
61
61
|
<pc-entity name="tree-0-trunk" position="-7 0.99 -6" scale="0.418 1.98 0.418"
|
|
62
62
|
><pc-render type="box" material="wood-material"></pc-render
|
|
63
63
|
><pc-collision type="box" half-extents="0.209 0.99 0.209"></pc-collision
|
|
64
|
-
><pc-
|
|
64
|
+
><pc-rigid-body type="static"></pc-rigid-body
|
|
65
65
|
></pc-entity>
|
|
66
66
|
<pc-entity name="tree-0-crown-0" position="-7 2.09 -6" scale="2.31 1.98 2.31"
|
|
67
67
|
><pc-render type="cone" material="leaves-material"></pc-render
|
|
@@ -75,7 +75,7 @@
|
|
|
75
75
|
<pc-entity name="tree-1-trunk" position="6.5 1.125 -6.5" scale="0.475 2.25 0.475"
|
|
76
76
|
><pc-render type="box" material="wood-material"></pc-render
|
|
77
77
|
><pc-collision type="box" half-extents="0.2375 1.125 0.2375"></pc-collision
|
|
78
|
-
><pc-
|
|
78
|
+
><pc-rigid-body type="static"></pc-rigid-body
|
|
79
79
|
></pc-entity>
|
|
80
80
|
<pc-entity name="tree-1-crown-0" position="6.5 2.375 -6.5" scale="2.625 2.25 2.625"
|
|
81
81
|
><pc-render type="cone" material="leaves-material"></pc-render
|
|
@@ -89,7 +89,7 @@
|
|
|
89
89
|
<pc-entity name="tree-2-trunk" position="-7.5 0.81 5" scale="0.342 1.62 0.342"
|
|
90
90
|
><pc-render type="box" material="wood-material"></pc-render
|
|
91
91
|
><pc-collision type="box" half-extents="0.171 0.81 0.171"></pc-collision
|
|
92
|
-
><pc-
|
|
92
|
+
><pc-rigid-body type="static"></pc-rigid-body
|
|
93
93
|
></pc-entity>
|
|
94
94
|
<pc-entity name="tree-2-crown-0" position="-7.5 1.71 5" scale="1.89 1.62 1.89"
|
|
95
95
|
><pc-render type="cone" material="leaves-material"></pc-render
|
|
@@ -103,7 +103,7 @@
|
|
|
103
103
|
<pc-entity name="tree-3-trunk" position="7 1.035 5.5" scale="0.437 2.07 0.437"
|
|
104
104
|
><pc-render type="box" material="wood-material"></pc-render
|
|
105
105
|
><pc-collision type="box" half-extents="0.2185 1.035 0.2185"></pc-collision
|
|
106
|
-
><pc-
|
|
106
|
+
><pc-rigid-body type="static"></pc-rigid-body
|
|
107
107
|
></pc-entity>
|
|
108
108
|
<pc-entity name="tree-3-crown-0" position="7 2.185 5.5" scale="2.415 2.07 2.415"
|
|
109
109
|
><pc-render type="cone" material="leaves-material"></pc-render
|
|
@@ -117,7 +117,7 @@
|
|
|
117
117
|
<pc-entity name="tree-4-trunk" position="-2.2 0.765 -8" scale="0.323 1.53 0.323"
|
|
118
118
|
><pc-render type="box" material="wood-material"></pc-render
|
|
119
119
|
><pc-collision type="box" half-extents="0.1615 0.765 0.1615"></pc-collision
|
|
120
|
-
><pc-
|
|
120
|
+
><pc-rigid-body type="static"></pc-rigid-body
|
|
121
121
|
></pc-entity>
|
|
122
122
|
<pc-entity name="tree-4-crown-0" position="-2.2 1.615 -8" scale="1.785 1.53 1.785"
|
|
123
123
|
><pc-render type="cone" material="leaves-material"></pc-render
|
|
@@ -131,16 +131,16 @@
|
|
|
131
131
|
<pc-entity name="fallen-log" position="0 0.35 6" scale="3.8 0.7 0.7"
|
|
132
132
|
><pc-render type="box" material="wood-material"></pc-render
|
|
133
133
|
><pc-collision type="box" half-extents="1.9 0.35 0.35"></pc-collision
|
|
134
|
-
><pc-
|
|
134
|
+
><pc-rigid-body type="static"></pc-rigid-body
|
|
135
135
|
></pc-entity>
|
|
136
136
|
<pc-entity name="stump" position="-1.8 0.4 -2.2" scale="0.9 0.8 0.9"
|
|
137
137
|
><pc-render type="box" material="wood-material"></pc-render
|
|
138
138
|
><pc-collision type="box" half-extents="0.45 0.4 0.45"></pc-collision
|
|
139
|
-
><pc-
|
|
139
|
+
><pc-rigid-body type="static"></pc-rigid-body
|
|
140
140
|
></pc-entity>
|
|
141
141
|
<pc-entity name="player" position="0 1.1 0">
|
|
142
142
|
<pc-collision type="capsule" radius="0.5" height="2.4" linear-offset="0 0.1 0"></pc-collision>
|
|
143
|
-
<pc-
|
|
143
|
+
<pc-rigid-body type="dynamic" mass="70" angular-factor="0 0 0"></pc-rigid-body>
|
|
144
144
|
<pc-entity name="character" position="0 0.2 0">
|
|
145
145
|
<pc-entity name="body" scale="0.56 0.88 0.38"
|
|
146
146
|
><pc-render type="box" material="shirt-material"></pc-render
|
|
@@ -6,7 +6,7 @@ import './starter.css';
|
|
|
6
6
|
|
|
7
7
|
await Promise.all([
|
|
8
8
|
whenReady('pc-entity[name="player"] > pc-collision'),
|
|
9
|
-
whenReady('pc-entity[name="player"] > pc-
|
|
9
|
+
whenReady('pc-entity[name="player"] > pc-rigid-body')
|
|
10
10
|
]);
|
|
11
11
|
const [player, camera, model] = await Promise.all([
|
|
12
12
|
whenReady<EntityElement>('pc-entity[name="player"]'),
|