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
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# PlayCanvas + TypeScript Starter
|
|
1
|
+
# PlayCanvas Engine + TypeScript Starter
|
|
2
2
|
|
|
3
3
|
A Vite-powered PlayCanvas Engine project with TypeScript, hot module replacement, ESLint and Prettier.
|
|
4
4
|
|
|
@@ -9,13 +9,13 @@ Node.js 22.23.2 or later.
|
|
|
9
9
|
## Getting started
|
|
10
10
|
|
|
11
11
|
```bash
|
|
12
|
-
npm create playcanvas@latest playcanvas-project -- --template
|
|
12
|
+
npm create playcanvas@latest playcanvas-project -- --template engine
|
|
13
13
|
cd playcanvas-project
|
|
14
14
|
npm install
|
|
15
15
|
npm run dev
|
|
16
16
|
```
|
|
17
17
|
|
|
18
|
-
Open <http://localhost:5173>. Edit `src
|
|
18
|
+
Open <http://localhost:5173>. Edit the files under `src/` and save to see the scene update.
|
|
19
19
|
|
|
20
20
|
## Scripts
|
|
21
21
|
|
|
@@ -28,24 +28,12 @@ Open <http://localhost:5173>. Edit `src/App.ts` to test hot module replacement.
|
|
|
28
28
|
| `npm run fmt` | Check formatting |
|
|
29
29
|
| `npm run typecheck` | Run TypeScript checks |
|
|
30
30
|
|
|
31
|
-
## Project structure
|
|
32
|
-
|
|
33
|
-
```text
|
|
34
|
-
.
|
|
35
|
-
├── public/
|
|
36
|
-
│ └── environment-map.png
|
|
37
|
-
├── src/
|
|
38
|
-
│ ├── assets/
|
|
39
|
-
│ ├── App.ts
|
|
40
|
-
│ ├── main.ts
|
|
41
|
-
│ └── style.css
|
|
42
|
-
├── eslint.config.mjs
|
|
43
|
-
├── index.html
|
|
44
|
-
└── tsconfig.json
|
|
45
|
-
```
|
|
46
|
-
|
|
47
31
|
Run `npm run build` to generate a deployable static site in `dist/`.
|
|
48
32
|
|
|
33
|
+
## Agent skills
|
|
34
|
+
|
|
35
|
+
This project includes [`@playcanvas/skills`](https://github.com/playcanvas/skills) under `.claude/skills/` and `.agents/skills/`, so Claude Code, Codex and Cursor pick up PlayCanvas-specific workflows automatically.
|
|
36
|
+
|
|
49
37
|
## Further reading
|
|
50
38
|
|
|
51
39
|
- [PlayCanvas Engine manual](https://developer.playcanvas.com/user-manual/engine/)
|
|
@@ -11,3 +11,9 @@ declare module 'playcanvas/scripts/esm/grid.mjs' {
|
|
|
11
11
|
|
|
12
12
|
export const Grid: typeof Script & (new () => Script);
|
|
13
13
|
}
|
|
14
|
+
|
|
15
|
+
declare module 'playcanvas/scripts/esm/sky/procedural-sky.mjs' {
|
|
16
|
+
import type { Script } from 'playcanvas';
|
|
17
|
+
|
|
18
|
+
export const ProceduralSky: typeof Script & (new () => Script);
|
|
19
|
+
}
|
|
@@ -16,7 +16,7 @@ document.querySelector<HTMLDivElement>('#root')!.innerHTML = `
|
|
|
16
16
|
<img src="${playcanvasLogo}" class="playcanvas-logo logo" alt="PlayCanvas logo" />
|
|
17
17
|
</a>
|
|
18
18
|
<a href="https://www.typescriptlang.org/" target="_blank">
|
|
19
|
-
<img src="${typescriptLogo}" class="logo
|
|
19
|
+
<img src="${typescriptLogo}" class="logo typescript" alt="TypeScript logo" />
|
|
20
20
|
</a>
|
|
21
21
|
</header>
|
|
22
22
|
</div>
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<link rel="icon" type="image/png" href="/src/assets/playcanvas.png" />
|
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
7
|
+
<title>PlayCanvas + TS</title>
|
|
8
|
+
</head>
|
|
9
|
+
<body>
|
|
10
|
+
<canvas id="application-canvas"></canvas>
|
|
11
|
+
<script type="module" src="/src/main.ts"></script>
|
|
12
|
+
</body>
|
|
13
|
+
</html>
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import type { ContainerResource } from 'playcanvas';
|
|
2
|
+
import {
|
|
3
|
+
AppBase,
|
|
4
|
+
AppOptions,
|
|
5
|
+
Asset,
|
|
6
|
+
AssetListLoader,
|
|
7
|
+
CameraComponentSystem,
|
|
8
|
+
ContainerHandler,
|
|
9
|
+
Entity,
|
|
10
|
+
FILLMODE_FILL_WINDOW,
|
|
11
|
+
RESOLUTION_AUTO,
|
|
12
|
+
RenderComponentSystem,
|
|
13
|
+
ScriptComponentSystem,
|
|
14
|
+
TONEMAP_ACES2,
|
|
15
|
+
TextureHandler,
|
|
16
|
+
createGraphicsDevice
|
|
17
|
+
} from 'playcanvas';
|
|
18
|
+
import { CameraControls } from 'playcanvas/scripts/esm/camera-controls.mjs';
|
|
19
|
+
import { ProceduralSky } from 'playcanvas/scripts/esm/sky/procedural-sky.mjs';
|
|
20
|
+
|
|
21
|
+
import './style.css';
|
|
22
|
+
|
|
23
|
+
// Swap this for your own model - anything you drop in public/ is served from the site root
|
|
24
|
+
const MODEL_URL = '/playcanvas-cube.glb';
|
|
25
|
+
|
|
26
|
+
const canvas = document.getElementById('application-canvas') as HTMLCanvasElement;
|
|
27
|
+
|
|
28
|
+
const device = await createGraphicsDevice(canvas);
|
|
29
|
+
device.maxPixelRatio = Math.min(window.devicePixelRatio, 2);
|
|
30
|
+
|
|
31
|
+
const createOptions = new AppOptions();
|
|
32
|
+
createOptions.graphicsDevice = device;
|
|
33
|
+
createOptions.componentSystems = [RenderComponentSystem, CameraComponentSystem, ScriptComponentSystem];
|
|
34
|
+
createOptions.resourceHandlers = [TextureHandler, ContainerHandler];
|
|
35
|
+
|
|
36
|
+
const app = new AppBase(canvas);
|
|
37
|
+
app.init(createOptions);
|
|
38
|
+
|
|
39
|
+
// Set the canvas to fill the window and automatically change resolution to be the same as the canvas size
|
|
40
|
+
app.setCanvasFillMode(FILLMODE_FILL_WINDOW);
|
|
41
|
+
app.setCanvasResolution(RESOLUTION_AUTO);
|
|
42
|
+
|
|
43
|
+
// Ensure canvas is resized when window changes size
|
|
44
|
+
const resize = () => app.resizeCanvas();
|
|
45
|
+
window.addEventListener('resize', resize);
|
|
46
|
+
app.on('destroy', () => {
|
|
47
|
+
window.removeEventListener('resize', resize);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
// Load the model before starting, so the first frame already has something in it
|
|
51
|
+
const model = new Asset('model', 'container', { url: MODEL_URL });
|
|
52
|
+
|
|
53
|
+
await new Promise<void>((resolve) => {
|
|
54
|
+
new AssetListLoader([model], app.assets).load(() => resolve());
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
app.start();
|
|
58
|
+
|
|
59
|
+
// A container asset holds the whole glTF scene, so instantiate it to get an entity hierarchy
|
|
60
|
+
app.root.addChild((model.resource as ContainerResource).instantiateRenderEntity());
|
|
61
|
+
|
|
62
|
+
// The procedural sky is both the background and the image-based lighting, so no light or
|
|
63
|
+
// environment map asset is needed. Its default luminance is scene-scale bright, so turn it down to
|
|
64
|
+
// keep the model out of clipping
|
|
65
|
+
const sky = new Entity('sky');
|
|
66
|
+
sky.addComponent('script');
|
|
67
|
+
sky.script?.create(ProceduralSky, {
|
|
68
|
+
properties: { luminance: 0.2 }
|
|
69
|
+
});
|
|
70
|
+
app.root.addChild(sky);
|
|
71
|
+
|
|
72
|
+
// Create camera entity with orbit controls. The sky is HDR, so it needs tone mapping to bring it
|
|
73
|
+
// back into displayable range - without it the whole image blows out to white
|
|
74
|
+
const camera = new Entity('camera');
|
|
75
|
+
camera.addComponent('camera', {
|
|
76
|
+
toneMapping: TONEMAP_ACES2
|
|
77
|
+
});
|
|
78
|
+
camera.setPosition(0, 0, 3);
|
|
79
|
+
camera.addComponent('script');
|
|
80
|
+
camera.script?.create(CameraControls);
|
|
81
|
+
app.root.addChild(camera);
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<link rel="icon" type="image/png" href="/src/assets/playcanvas.png" />
|
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
7
|
+
<title>PlayCanvas + TS</title>
|
|
8
|
+
</head>
|
|
9
|
+
<body>
|
|
10
|
+
<canvas id="application-canvas"></canvas>
|
|
11
|
+
<script type="module" src="/src/main.ts"></script>
|
|
12
|
+
</body>
|
|
13
|
+
</html>
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AppBase,
|
|
3
|
+
AppOptions,
|
|
4
|
+
CameraComponentSystem,
|
|
5
|
+
Color,
|
|
6
|
+
ContainerHandler,
|
|
7
|
+
Entity,
|
|
8
|
+
FILLMODE_FILL_WINDOW,
|
|
9
|
+
LightComponentSystem,
|
|
10
|
+
RESOLUTION_AUTO,
|
|
11
|
+
RenderComponentSystem,
|
|
12
|
+
TextureHandler,
|
|
13
|
+
createGraphicsDevice
|
|
14
|
+
} from 'playcanvas';
|
|
15
|
+
|
|
16
|
+
import './style.css';
|
|
17
|
+
|
|
18
|
+
const canvas = document.getElementById('application-canvas') as HTMLCanvasElement;
|
|
19
|
+
|
|
20
|
+
const device = await createGraphicsDevice(canvas);
|
|
21
|
+
device.maxPixelRatio = Math.min(window.devicePixelRatio, 2);
|
|
22
|
+
|
|
23
|
+
const createOptions = new AppOptions();
|
|
24
|
+
createOptions.graphicsDevice = device;
|
|
25
|
+
createOptions.componentSystems = [RenderComponentSystem, CameraComponentSystem, LightComponentSystem];
|
|
26
|
+
createOptions.resourceHandlers = [TextureHandler, ContainerHandler];
|
|
27
|
+
|
|
28
|
+
const app = new AppBase(canvas);
|
|
29
|
+
app.init(createOptions);
|
|
30
|
+
app.start();
|
|
31
|
+
|
|
32
|
+
// Set the canvas to fill the window and automatically change resolution to be the same as the canvas size
|
|
33
|
+
app.setCanvasFillMode(FILLMODE_FILL_WINDOW);
|
|
34
|
+
app.setCanvasResolution(RESOLUTION_AUTO);
|
|
35
|
+
|
|
36
|
+
// Ensure canvas is resized when window changes size
|
|
37
|
+
const resize = () => app.resizeCanvas();
|
|
38
|
+
window.addEventListener('resize', resize);
|
|
39
|
+
app.on('destroy', () => {
|
|
40
|
+
window.removeEventListener('resize', resize);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
// Create box entity
|
|
44
|
+
const box = new Entity('cube');
|
|
45
|
+
box.addComponent('render', {
|
|
46
|
+
type: 'box'
|
|
47
|
+
});
|
|
48
|
+
app.root.addChild(box);
|
|
49
|
+
|
|
50
|
+
// Create camera entity
|
|
51
|
+
const camera = new Entity('camera');
|
|
52
|
+
camera.addComponent('camera', {
|
|
53
|
+
clearColor: new Color(0.5, 0.6, 0.9)
|
|
54
|
+
});
|
|
55
|
+
camera.setPosition(0, 0, 3);
|
|
56
|
+
app.root.addChild(camera);
|
|
57
|
+
|
|
58
|
+
// Create directional light entity
|
|
59
|
+
const light = new Entity('light');
|
|
60
|
+
light.addComponent('light');
|
|
61
|
+
light.setEulerAngles(45, 0, 0);
|
|
62
|
+
app.root.addChild(light);
|
|
63
|
+
|
|
64
|
+
// Rotate the box according to the delta time since the last frame
|
|
65
|
+
app.on('update', (dt: number) => box.rotate(10 * dt, 20 * dt, 30 * dt));
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<link rel="icon" type="image/png" href="/src/assets/playcanvas.png" />
|
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
7
|
+
<title>PlayCanvas + TS</title>
|
|
8
|
+
</head>
|
|
9
|
+
<body>
|
|
10
|
+
<canvas id="application-canvas"></canvas>
|
|
11
|
+
<script type="module" src="/src/main.ts"></script>
|
|
12
|
+
</body>
|
|
13
|
+
</html>
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AppBase,
|
|
3
|
+
AppOptions,
|
|
4
|
+
Asset,
|
|
5
|
+
AssetListLoader,
|
|
6
|
+
CameraComponentSystem,
|
|
7
|
+
Entity,
|
|
8
|
+
FILLMODE_FILL_WINDOW,
|
|
9
|
+
GSplatComponentSystem,
|
|
10
|
+
GSplatHandler,
|
|
11
|
+
RESOLUTION_AUTO,
|
|
12
|
+
ScriptComponentSystem,
|
|
13
|
+
TextureHandler,
|
|
14
|
+
createGraphicsDevice
|
|
15
|
+
} from 'playcanvas';
|
|
16
|
+
import { CameraControls } from 'playcanvas/scripts/esm/camera-controls.mjs';
|
|
17
|
+
|
|
18
|
+
import './style.css';
|
|
19
|
+
|
|
20
|
+
// Hosted so the template stays small. Capture or edit your own splat at https://superspl.at, then
|
|
21
|
+
// drop the .sog or .ply in public/ and point this at it instead
|
|
22
|
+
const SPLAT_URL = 'https://developer.playcanvas.com/assets/toy-cat.sog';
|
|
23
|
+
|
|
24
|
+
const canvas = document.getElementById('application-canvas') as HTMLCanvasElement;
|
|
25
|
+
|
|
26
|
+
// Antialiasing is disabled - splat rendering is fragment bound, so MSAA costs a lot and adds little
|
|
27
|
+
const device = await createGraphicsDevice(canvas, { antialias: false });
|
|
28
|
+
device.maxPixelRatio = Math.min(window.devicePixelRatio, 2);
|
|
29
|
+
|
|
30
|
+
const createOptions = new AppOptions();
|
|
31
|
+
createOptions.graphicsDevice = device;
|
|
32
|
+
createOptions.componentSystems = [CameraComponentSystem, GSplatComponentSystem, ScriptComponentSystem];
|
|
33
|
+
|
|
34
|
+
// TextureHandler is required as well as GSplatHandler - a .sog is a container whose splat data is
|
|
35
|
+
// stored as textures, and without it the splat loads but silently renders nothing
|
|
36
|
+
createOptions.resourceHandlers = [TextureHandler, GSplatHandler];
|
|
37
|
+
|
|
38
|
+
const app = new AppBase(canvas);
|
|
39
|
+
app.init(createOptions);
|
|
40
|
+
|
|
41
|
+
// Set the canvas to fill the window and automatically change resolution to be the same as the canvas size
|
|
42
|
+
app.setCanvasFillMode(FILLMODE_FILL_WINDOW);
|
|
43
|
+
app.setCanvasResolution(RESOLUTION_AUTO);
|
|
44
|
+
|
|
45
|
+
// Ensure canvas is resized when window changes size
|
|
46
|
+
const resize = () => app.resizeCanvas();
|
|
47
|
+
window.addEventListener('resize', resize);
|
|
48
|
+
app.on('destroy', () => {
|
|
49
|
+
window.removeEventListener('resize', resize);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
// Load the splat before starting, so the first frame already has something in it
|
|
53
|
+
const splat = new Asset('splat', 'gsplat', { url: SPLAT_URL });
|
|
54
|
+
|
|
55
|
+
await new Promise<void>((resolve) => {
|
|
56
|
+
new AssetListLoader([splat], app.assets).load(() => resolve());
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
app.start();
|
|
60
|
+
|
|
61
|
+
// Splats are captured in their own space, so this one needs offsetting and flipping upright
|
|
62
|
+
const toyCat = new Entity('toy cat');
|
|
63
|
+
toyCat.addComponent('gsplat', { asset: splat });
|
|
64
|
+
toyCat.setPosition(0, -0.7, 0);
|
|
65
|
+
toyCat.setEulerAngles(0, 0, 180);
|
|
66
|
+
app.root.addChild(toyCat);
|
|
67
|
+
|
|
68
|
+
// Create camera entity with orbit controls
|
|
69
|
+
const camera = new Entity('camera');
|
|
70
|
+
camera.addComponent('camera');
|
|
71
|
+
camera.setPosition(0, 0, 2.5);
|
|
72
|
+
camera.addComponent('script');
|
|
73
|
+
camera.script?.create(CameraControls);
|
|
74
|
+
app.root.addChild(camera);
|
package/templates/index.ts
CHANGED
|
@@ -1,18 +1,50 @@
|
|
|
1
1
|
import colors from 'picocolors';
|
|
2
2
|
|
|
3
|
-
const { cyan, yellow } = colors;
|
|
3
|
+
const { cyan, magenta, yellow } = colors;
|
|
4
4
|
|
|
5
5
|
export const TEMPLATES = [
|
|
6
6
|
{
|
|
7
|
-
name: '
|
|
8
|
-
display: '
|
|
9
|
-
description: 'with PlayCanvas Engine',
|
|
7
|
+
name: 'engine',
|
|
8
|
+
display: 'Engine',
|
|
9
|
+
description: 'with the PlayCanvas Engine API directly',
|
|
10
10
|
color: yellow
|
|
11
11
|
},
|
|
12
12
|
{
|
|
13
|
-
name: 'react
|
|
13
|
+
name: 'react',
|
|
14
14
|
display: 'React',
|
|
15
15
|
description: 'with @playcanvas/react',
|
|
16
16
|
color: cyan
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
name: 'web-components',
|
|
20
|
+
display: 'Web Components',
|
|
21
|
+
description: 'with @playcanvas/web-components',
|
|
22
|
+
color: magenta
|
|
23
|
+
}
|
|
24
|
+
];
|
|
25
|
+
|
|
26
|
+
// Every template holds a `base` directory of shared tooling plus one directory per boilerplate,
|
|
27
|
+
// which is copied over the top. Assets a boilerplate needs in every template live once in
|
|
28
|
+
// `_shared/<boilerplate>` and are seeded in first. The first entry is the default.
|
|
29
|
+
export const BOILERPLATES = [
|
|
30
|
+
{
|
|
31
|
+
name: 'spinning-cube',
|
|
32
|
+
display: 'Spinning Cube',
|
|
33
|
+
description: 'a rotating cube and a light'
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
name: 'interactive-sphere',
|
|
37
|
+
display: 'Interactive Sphere',
|
|
38
|
+
description: 'orbit controls, a grid and pointer picking'
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
name: 'model-viewer',
|
|
42
|
+
display: 'Model Viewer',
|
|
43
|
+
description: 'a glTF model, orbit controls and a procedural sky'
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
name: 'splat-viewer',
|
|
47
|
+
display: 'Splat Viewer',
|
|
48
|
+
description: 'a Gaussian splat and orbit controls'
|
|
17
49
|
}
|
|
18
50
|
];
|
|
@@ -9,13 +9,13 @@ Node.js 22.23.2 or later.
|
|
|
9
9
|
## Getting started
|
|
10
10
|
|
|
11
11
|
```bash
|
|
12
|
-
npm create playcanvas@latest playcanvas-project -- --template react
|
|
12
|
+
npm create playcanvas@latest playcanvas-project -- --template react
|
|
13
13
|
cd playcanvas-project
|
|
14
14
|
npm install
|
|
15
15
|
npm run dev
|
|
16
16
|
```
|
|
17
17
|
|
|
18
|
-
Open <http://localhost:5173>. Edit `src
|
|
18
|
+
Open <http://localhost:5173>. Edit the files under `src/` and save to see the scene update.
|
|
19
19
|
|
|
20
20
|
## Scripts
|
|
21
21
|
|
|
@@ -28,25 +28,12 @@ Open <http://localhost:5173>. Edit `src/Scene.tsx` to test hot module replacemen
|
|
|
28
28
|
| `npm run fmt` | Check formatting |
|
|
29
29
|
| `npm run typecheck` | Run TypeScript checks |
|
|
30
30
|
|
|
31
|
-
## Project structure
|
|
32
|
-
|
|
33
|
-
```text
|
|
34
|
-
.
|
|
35
|
-
├── public/
|
|
36
|
-
│ └── environment-map.png
|
|
37
|
-
├── src/
|
|
38
|
-
│ ├── assets/
|
|
39
|
-
│ ├── App.tsx
|
|
40
|
-
│ ├── Scene.tsx
|
|
41
|
-
│ └── main.tsx
|
|
42
|
-
├── eslint.config.mjs
|
|
43
|
-
├── index.html
|
|
44
|
-
├── tsconfig.json
|
|
45
|
-
└── vite.config.ts
|
|
46
|
-
```
|
|
47
|
-
|
|
48
31
|
Run `npm run build` to generate a deployable static site in `dist/`.
|
|
49
32
|
|
|
33
|
+
## Agent skills
|
|
34
|
+
|
|
35
|
+
This project includes [`@playcanvas/skills`](https://github.com/playcanvas/skills) under `.claude/skills/` and `.agents/skills/`, so Claude Code, Codex and Cursor pick up PlayCanvas-specific workflows automatically.
|
|
36
|
+
|
|
50
37
|
## Further reading
|
|
51
38
|
|
|
52
39
|
- [PlayCanvas React manual](https://developer.playcanvas.com/user-manual/react/)
|
|
@@ -11,3 +11,9 @@ declare module 'playcanvas/scripts/esm/grid.mjs' {
|
|
|
11
11
|
|
|
12
12
|
export const Grid: typeof Script & (new () => Script);
|
|
13
13
|
}
|
|
14
|
+
|
|
15
|
+
declare module 'playcanvas/scripts/esm/sky/procedural-sky.mjs' {
|
|
16
|
+
import type { Script } from 'playcanvas';
|
|
17
|
+
|
|
18
|
+
export const ProceduralSky: typeof Script & (new () => Script);
|
|
19
|
+
}
|
|
@@ -9,7 +9,6 @@ import { useEffect, useState } from 'react';
|
|
|
9
9
|
* The Scene renders a sphere with a grid and camera controls
|
|
10
10
|
*/
|
|
11
11
|
function Scene({ onClick }: SceneProps) {
|
|
12
|
-
// Track the hover state and set the color based on the hover state
|
|
13
12
|
const [hovering, setHovering] = useState(false);
|
|
14
13
|
|
|
15
14
|
// Set a material color based on the hover state
|
|
@@ -21,7 +20,7 @@ function Scene({ onClick }: SceneProps) {
|
|
|
21
20
|
// Load the environment map
|
|
22
21
|
const { asset: envMap } = useEnvAtlas('/environment-map.png');
|
|
23
22
|
|
|
24
|
-
//
|
|
23
|
+
// Change the mouse cursor based on the hover state
|
|
25
24
|
useEffect(() => {
|
|
26
25
|
document.body.style.cursor = hovering ? 'pointer' : 'default';
|
|
27
26
|
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<link rel="icon" type="image/png" href="/src/assets/playcanvas.png" />
|
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
7
|
+
<title>PlayCanvas + React</title>
|
|
8
|
+
</head>
|
|
9
|
+
<body>
|
|
10
|
+
<div id="root"></div>
|
|
11
|
+
<script type="module" src="/src/main.tsx"></script>
|
|
12
|
+
</body>
|
|
13
|
+
</html>
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { Application, Container, Entity } from '@playcanvas/react';
|
|
2
|
+
import { Camera, Script } from '@playcanvas/react/components';
|
|
3
|
+
import { useModel } from '@playcanvas/react/hooks';
|
|
4
|
+
import { TONEMAP_ACES2 } from 'playcanvas';
|
|
5
|
+
import { CameraControls } from 'playcanvas/scripts/esm/camera-controls.mjs';
|
|
6
|
+
import { ProceduralSky } from 'playcanvas/scripts/esm/sky/procedural-sky.mjs';
|
|
7
|
+
|
|
8
|
+
// Swap this for your own model - anything you drop in public/ is served from the site root
|
|
9
|
+
const MODEL_URL = '/playcanvas-cube.glb';
|
|
10
|
+
|
|
11
|
+
function Model() {
|
|
12
|
+
const { asset } = useModel(MODEL_URL);
|
|
13
|
+
|
|
14
|
+
// Don't render until the model is loaded
|
|
15
|
+
if (!asset) return null;
|
|
16
|
+
|
|
17
|
+
return <Container asset={asset} />;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function App() {
|
|
21
|
+
return (
|
|
22
|
+
<Application>
|
|
23
|
+
{/* The procedural sky is both the background and the image-based lighting, so no
|
|
24
|
+
light or environment map asset is needed. Its default luminance is scene-scale
|
|
25
|
+
bright, so turn it down to keep the model out of clipping */}
|
|
26
|
+
<Entity name="sky">
|
|
27
|
+
<Script script={ProceduralSky} luminance={0.2} />
|
|
28
|
+
</Entity>
|
|
29
|
+
|
|
30
|
+
{/* Create a camera entity with orbit controls. The sky is HDR, so it needs tone
|
|
31
|
+
mapping to bring it back into displayable range - without it the whole image blows
|
|
32
|
+
out to white */}
|
|
33
|
+
<Entity name="camera" position={[0, 0, 3]}>
|
|
34
|
+
<Camera toneMapping={TONEMAP_ACES2} />
|
|
35
|
+
<Script script={CameraControls} />
|
|
36
|
+
</Entity>
|
|
37
|
+
|
|
38
|
+
<Model />
|
|
39
|
+
</Application>
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export default App;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<link rel="icon" type="image/png" href="/src/assets/playcanvas.png" />
|
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
7
|
+
<title>PlayCanvas + React</title>
|
|
8
|
+
</head>
|
|
9
|
+
<body>
|
|
10
|
+
<div id="root"></div>
|
|
11
|
+
<script type="module" src="/src/main.tsx"></script>
|
|
12
|
+
</body>
|
|
13
|
+
</html>
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { Application, Entity } from '@playcanvas/react';
|
|
2
|
+
import { Camera, Light, Render } from '@playcanvas/react/components';
|
|
3
|
+
import { useAppEvent } from '@playcanvas/react/hooks';
|
|
4
|
+
import type { Entity as PcEntity } from 'playcanvas';
|
|
5
|
+
import { useRef } from 'react';
|
|
6
|
+
|
|
7
|
+
function Scene() {
|
|
8
|
+
const cube = useRef<PcEntity>(null);
|
|
9
|
+
|
|
10
|
+
// Rotate the cube according to the delta time since the last frame
|
|
11
|
+
useAppEvent('update', (dt: number) => cube.current?.rotate(10 * dt, 20 * dt, 30 * dt));
|
|
12
|
+
|
|
13
|
+
return (
|
|
14
|
+
<>
|
|
15
|
+
<Entity name="camera" position={[0, 0, 3]}>
|
|
16
|
+
<Camera clearColor="#8099e6" />
|
|
17
|
+
</Entity>
|
|
18
|
+
<Entity name="light" rotation={[45, 0, 0]}>
|
|
19
|
+
<Light type="directional" />
|
|
20
|
+
</Entity>
|
|
21
|
+
<Entity name="cube" ref={cube}>
|
|
22
|
+
<Render type="box" />
|
|
23
|
+
</Entity>
|
|
24
|
+
</>
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function App() {
|
|
29
|
+
return (
|
|
30
|
+
<Application>
|
|
31
|
+
<Scene />
|
|
32
|
+
</Application>
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export default App;
|