Skip to content

Commit f09252f

Browse files
authored
CameraControls (previously MultiCamera) (playcanvas#7111)
* Adds flags to enable and disable orbit fly and pan on multicamera * Added smoothing variable to focus function * Cleaned up mouse button logic * Disabled fly moving if not flying * Updated enable fly * Disabled zooming if panning or orbit are disabled * Updated multi camera description * Added smoothed focus setting * Added pitch constraints and added refocus method and better multicam default setup * Widened controls and added finer precisino to multi camera controls * Added zoomDistMin and zoomDistMax and added attribute comments * Removed zoomDistMin and zoomDistMax * removed hidden flag * Updated focusPoint to be getter/setter * renamed super to this * removed zoomDist assign in ctor * Replaced individual pitch with camera pitch range * Renamed camera pitch range to pitchRange * Merged base camera into multi-camera * Renamed multi-camera to camera controls * Fixed linting issue * Updated zoom range to allow for infinite maximum * Updated comments and refactored smoothing apply functions to be reused in setters for zoom and pitch constraints * Updated smoothing to smooth variable * Fixed potential near clip issue * Refactor for description * Updated orbit camera example * Updated fly camera example * Fixed trailing commas * Updated fly and orbit thumbnails * Fixed focusPoint issue * Updated get focus point * Added in ability to change element to listen on * Added pointer capture when mouse is down * updated camera controls elev calc
1 parent 878d9d7 commit f09252f

14 files changed

Lines changed: 1545 additions & 905 deletions
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
/**
2+
* @param {import('../../app/components/Example.mjs').ControlOptions} options - The options.
3+
* @returns {JSX.Element} The returned JSX Element.
4+
*/
5+
export const controls = ({ observer, ReactPCUI, jsx, fragment }) => {
6+
const { BindingTwoWay, LabelGroup, Panel, SliderInput, VectorInput } = ReactPCUI;
7+
8+
return fragment(
9+
jsx(
10+
Panel,
11+
{ headerText: 'Attributes' },
12+
jsx(
13+
LabelGroup,
14+
{ text: 'Look sensitivity' },
15+
jsx(SliderInput, {
16+
binding: new BindingTwoWay(),
17+
link: { observer, path: 'attr.lookSensitivity' },
18+
min: 0.1,
19+
max: 1,
20+
step: 0.01
21+
})
22+
),
23+
jsx(
24+
LabelGroup,
25+
{ text: 'Look damping' },
26+
jsx(SliderInput, {
27+
binding: new BindingTwoWay(),
28+
link: { observer, path: 'attr.lookDamping' },
29+
min: 0,
30+
max: 0.999,
31+
step: 0.001,
32+
precision: 3
33+
})
34+
),
35+
jsx(
36+
LabelGroup,
37+
{ text: 'Move damping' },
38+
jsx(SliderInput, {
39+
binding: new BindingTwoWay(),
40+
link: { observer, path: 'attr.moveDamping' },
41+
min: 0,
42+
max: 0.999,
43+
step: 0.001,
44+
precision: 3
45+
})
46+
),
47+
jsx(
48+
LabelGroup,
49+
{ text: 'Pitch range' },
50+
jsx(VectorInput, {
51+
binding: new BindingTwoWay(),
52+
link: { observer, path: 'attr.pitchRange' },
53+
dimensions: 2
54+
})
55+
),
56+
jsx(
57+
LabelGroup,
58+
{ text: 'Move speed' },
59+
jsx(SliderInput, {
60+
binding: new BindingTwoWay(),
61+
link: { observer, path: 'attr.moveSpeed' },
62+
min: 1,
63+
max: 10
64+
})
65+
),
66+
jsx(
67+
LabelGroup,
68+
{ text: 'Sprint speed' },
69+
jsx(SliderInput, {
70+
binding: new BindingTwoWay(),
71+
link: { observer, path: 'attr.sprintSpeed' },
72+
min: 1,
73+
max: 10
74+
})
75+
),
76+
jsx(
77+
LabelGroup,
78+
{ text: 'Crouch speed' },
79+
jsx(SliderInput, {
80+
binding: new BindingTwoWay(),
81+
link: { observer, path: 'attr.crouchSpeed' },
82+
min: 1,
83+
max: 10
84+
})
85+
)
86+
)
87+
);
88+
};
Lines changed: 108 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,14 @@
1-
import { deviceType, rootPath } from 'examples/utils';
1+
// @config DESCRIPTION <div style='text-align:center'><div>(<b>WASDQE</b>) Move</div></div>
2+
import { data } from 'examples/observer';
3+
import { deviceType, rootPath, fileImport } from 'examples/utils';
24
import * as pc from 'playcanvas';
35

4-
const canvas = /** @type {HTMLCanvasElement} */ (document.getElementById('application-canvas'));
6+
const { CameraControls } = await fileImport(`${rootPath}/static/scripts/camera-controls.mjs`);
7+
8+
const canvas = document.getElementById('application-canvas');
9+
if (!(canvas instanceof HTMLCanvasElement)) {
10+
throw new Error('No canvas found');
11+
}
512
window.focus();
613

714
const gfxOptions = {
@@ -10,21 +17,29 @@ const gfxOptions = {
1017
twgslUrl: `${rootPath}/static/lib/twgsl/twgsl.js`
1118
};
1219

20+
const assets = {
21+
helipad: new pc.Asset(
22+
'helipad-env-atlas',
23+
'texture',
24+
{ url: `${rootPath}/static/assets/cubemaps/helipad-env-atlas.png` },
25+
{ type: pc.TEXTURETYPE_RGBP, mipmaps: false }
26+
),
27+
statue: new pc.Asset('statue', 'container', { url: `${rootPath}/static/assets/models/statue.glb` })
28+
};
29+
1330
const device = await pc.createGraphicsDevice(canvas, gfxOptions);
1431
device.maxPixelRatio = Math.min(window.devicePixelRatio, 2);
1532

1633
const createOptions = new pc.AppOptions();
1734
createOptions.graphicsDevice = device;
18-
createOptions.mouse = new pc.Mouse(document.body);
19-
createOptions.keyboard = new pc.Keyboard(window);
2035

2136
createOptions.componentSystems = [
2237
pc.RenderComponentSystem,
2338
pc.CameraComponentSystem,
2439
pc.LightComponentSystem,
2540
pc.ScriptComponentSystem
2641
];
27-
createOptions.resourceHandlers = [pc.ScriptHandler];
42+
createOptions.resourceHandlers = [pc.TextureHandler, pc.ContainerHandler, pc.ScriptHandler];
2843

2944
const app = new pc.AppBase(canvas);
3045
app.init(createOptions);
@@ -40,105 +55,112 @@ app.on('destroy', () => {
4055
window.removeEventListener('resize', resize);
4156
});
4257

43-
const assets = {
44-
script: new pc.Asset('script', 'script', { url: `${rootPath}/static/scripts/camera/fly-camera.js` })
45-
};
58+
await new Promise((resolve) => {
59+
new pc.AssetListLoader(Object.values(assets), app.assets).load(resolve);
60+
});
4661

4762
/**
48-
* @param {pc.Asset[] | number[]} assetList - The asset list.
49-
* @param {pc.AssetRegistry} assetRegistry - The asset registry.
50-
* @returns {Promise<void>} The promise.
63+
* Calculate the bounding box of an entity.
64+
*
65+
* @param {pc.BoundingBox} bbox - The bounding box.
66+
* @param {pc.Entity} entity - The entity.
67+
* @returns {pc.BoundingBox} The bounding box.
5168
*/
52-
function loadAssets(assetList, assetRegistry) {
53-
return new Promise((resolve) => {
54-
const assetListLoader = new pc.AssetListLoader(assetList, assetRegistry);
55-
assetListLoader.load(resolve);
69+
const calcEntityAABB = (bbox, entity) => {
70+
bbox.center.set(0, 0, 0);
71+
bbox.halfExtents.set(0, 0, 0);
72+
entity.findComponents('render').forEach((render) => {
73+
render.meshInstances.forEach((/** @type {pc.MeshInstance} */ mi) => {
74+
bbox.add(mi.aabb);
75+
});
5676
});
57-
}
58-
await loadAssets(Object.values(assets), app.assets);
59-
app.scene.ambientLight = new pc.Color(0.2, 0.2, 0.2);
60-
app.start();
61-
62-
// *********** Helper functions *******************
63-
/**
64-
* @param {pc.Color} color - The color.
65-
* @returns {pc.StandardMaterial} The material.
66-
*/
67-
function createMaterial(color) {
68-
const material = new pc.StandardMaterial();
69-
material.diffuse = color;
70-
// we need to call material.update when we change its properties
71-
material.update();
72-
return material;
73-
}
77+
return bbox;
78+
};
7479

7580
/**
76-
* @param {pc.Vec3} position - The position.
77-
* @param {pc.Vec3} size - The size.
78-
* @param {pc.Material} material - The material.
81+
* @param {pc.Entity} focus - The entity to focus the camera on.
82+
* @returns {CameraControls} The camera-controls script.
7983
*/
80-
function createBox(position, size, material) {
81-
// create an entity and add a model component of type 'box'
82-
const box = new pc.Entity();
83-
box.addComponent('render', {
84-
type: 'box',
85-
material: material
84+
const createFlyCamera = (focus) => {
85+
const camera = new pc.Entity();
86+
camera.addComponent('camera');
87+
camera.addComponent('script');
88+
camera.setPosition(0, 20, 30);
89+
app.root.addChild(camera);
90+
91+
const bbox = calcEntityAABB(new pc.BoundingBox(), focus);
92+
93+
/** @type {CameraControls} */
94+
const script = camera.script.create(CameraControls, {
95+
attributes: {
96+
enableOrbit: false,
97+
enablePan: false,
98+
focusPoint: bbox.center,
99+
sceneSize: bbox.halfExtents.length(),
100+
pitchRange: new pc.Vec2(-90, 90)
101+
}
86102
});
87103

88-
// move the box
89-
box.setLocalPosition(position);
90-
box.setLocalScale(size);
91-
92-
// add the box to the hierarchy
93-
app.root.addChild(box);
94-
}
95-
96-
// *********** Create Boxes *******************
104+
return script;
105+
};
97106

98-
// create a few boxes in our scene
99-
const red = createMaterial(pc.Color.RED);
100-
for (let i = 0; i < 3; i++) {
101-
for (let j = 0; j < 2; j++) {
102-
createBox(new pc.Vec3(i * 2, 0, j * 4), pc.Vec3.ONE, red);
103-
}
104-
}
107+
app.start();
105108

106-
// create a floor
107-
const white = createMaterial(pc.Color.WHITE);
108-
createBox(new pc.Vec3(0, -0.5, 0), new pc.Vec3(10, 0.1, 10), white);
109+
app.scene.ambientLight.set(0.4, 0.4, 0.4);
109110

110-
// *********** Create lights *******************
111+
app.scene.skyboxMip = 1;
112+
app.scene.skyboxIntensity = 0.4;
113+
app.scene.envAtlas = assets.helipad.resource;
111114

112-
// make our scene prettier by adding a directional light
115+
// Create a directional light
113116
const light = new pc.Entity();
114-
light.addComponent('light', {
115-
type: 'omni',
116-
color: new pc.Color(1, 1, 1),
117-
range: 100
118-
});
119-
light.setLocalPosition(0, 0, 2);
120-
121-
// add the light to the hierarchy
117+
light.addComponent('light');
118+
light.setLocalEulerAngles(45, 30, 0);
122119
app.root.addChild(light);
123120

124-
// *********** Create camera *******************
125-
126-
// Create an Entity with a camera component
127-
const camera = new pc.Entity();
128-
camera.addComponent('camera', {
129-
clearColor: new pc.Color(0.5, 0.5, 0.8),
130-
nearClip: 0.3,
131-
farClip: 30
132-
});
121+
const statue = assets.statue.resource.instantiateRenderEntity();
122+
statue.setLocalPosition(0, -0.5, 0);
123+
app.root.addChild(statue);
124+
125+
const multiCameraScript = createFlyCamera(statue);
126+
127+
data.set('attr', [
128+
'lookSensitivity',
129+
'lookDamping',
130+
'moveDamping',
131+
'pitchRange',
132+
'moveSpeed',
133+
'sprintSpeed',
134+
'crouchSpeed'
135+
].reduce((/** @type {Record<string, any>} */ obj, key) => {
136+
const value = multiCameraScript[key];
137+
138+
if (value instanceof pc.Vec2) {
139+
obj[key] = [value.x, value.y];
140+
return obj;
141+
}
133142

134-
// add the fly camera script to the camera
135-
camera.addComponent('script');
136-
camera.script.create('flyCamera');
143+
obj[key] = multiCameraScript[key];
144+
return obj;
145+
}, {}));
137146

138-
// add the camera to the hierarchy
139-
app.root.addChild(camera);
147+
const tmpVa = new pc.Vec2();
148+
data.on('*:set', (/** @type {string} */ path, /** @type {any} */ value) => {
149+
const [category, key, index] = path.split('.');
150+
if (category !== 'attr') {
151+
return;
152+
}
140153

141-
// Move the camera a little further away
142-
camera.translate(2, 0.8, 9);
154+
if (Array.isArray(value)) {
155+
multiCameraScript[key] = tmpVa.set(value[0], value[1]);
156+
return;
157+
}
158+
if (index !== undefined) {
159+
const arr = data.get(`${category}.${key}`);
160+
multiCameraScript[key] = tmpVa.set(arr[0], arr[1]);
161+
return;
162+
}
163+
multiCameraScript[key] = value;
164+
});
143165

144166
export { app };

0 commit comments

Comments
 (0)