Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

CameraControls (previously MultiCamera) #7111

Open
wants to merge 34 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
48d7228
Adds flags to enable and disable orbit fly and pan on multicamera
kpal81xd Nov 13, 2024
67b8423
Added smoothing variable to focus function
kpal81xd Nov 13, 2024
4e4241e
Cleaned up mouse button logic
kpal81xd Nov 13, 2024
cd0e4e6
Disabled fly moving if not flying
kpal81xd Nov 14, 2024
c89b812
Updated enable fly
kpal81xd Nov 14, 2024
a7c344c
Disabled zooming if panning or orbit are disabled
kpal81xd Nov 14, 2024
5c322b6
Updated multi camera description
kpal81xd Nov 14, 2024
1542d32
Added smoothed focus setting
kpal81xd Nov 14, 2024
e026981
Added pitch constraints and added refocus method and better multicam …
kpal81xd Nov 14, 2024
af7f34b
Widened controls and added finer precisino to multi camera controls
kpal81xd Nov 14, 2024
a049b82
Added zoomDistMin and zoomDistMax and added attribute comments
kpal81xd Nov 14, 2024
d81ce41
Removed zoomDistMin and zoomDistMax
kpal81xd Nov 14, 2024
6b95ce4
removed hidden flag
kpal81xd Nov 14, 2024
34b2c6b
Merge branch 'main' into multicam-api
kpal81xd Nov 14, 2024
c3e8b8c
Merge branch 'main' into multicam-api
kpal81xd Nov 14, 2024
abf3a2c
Updated focusPoint to be getter/setter
kpal81xd Nov 14, 2024
f9cde3f
renamed super to this
kpal81xd Nov 14, 2024
8b91100
removed zoomDist assign in ctor
kpal81xd Nov 14, 2024
40fe0f9
Replaced individual pitch with camera pitch range
kpal81xd Nov 15, 2024
427bc4e
Renamed camera pitch range to pitchRange
kpal81xd Nov 15, 2024
08f5bce
Merged base camera into multi-camera
kpal81xd Nov 15, 2024
9d71532
Renamed multi-camera to camera controls
kpal81xd Nov 15, 2024
c2b4b56
Fixed linting issue
kpal81xd Nov 15, 2024
3113526
Updated zoom range to allow for infinite maximum
kpal81xd Nov 15, 2024
053a682
Updated comments and refactored smoothing apply functions to be reuse…
kpal81xd Nov 15, 2024
5161f77
Updated smoothing to smooth variable
kpal81xd Nov 15, 2024
c40a184
Merge branch 'main' into multicam-api
kpal81xd Nov 15, 2024
fc4112d
Fixed potential near clip issue
kpal81xd Nov 15, 2024
4ff769d
Refactor for description
kpal81xd Nov 15, 2024
4d1cbc2
Updated orbit camera example
kpal81xd Nov 15, 2024
4eb895c
Updated fly camera example
kpal81xd Nov 15, 2024
824f01f
Fixed trailing commas
kpal81xd Nov 15, 2024
536ca54
Updated fly and orbit thumbnails
kpal81xd Nov 15, 2024
0a85cca
Fixed focusPoint issue
kpal81xd Nov 15, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 88 additions & 0 deletions examples/src/examples/camera/fly.controls.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
/**
* @param {import('../../app/components/Example.mjs').ControlOptions} options - The options.
* @returns {JSX.Element} The returned JSX Element.
*/
export const controls = ({ observer, ReactPCUI, jsx, fragment }) => {
const { BindingTwoWay, LabelGroup, Panel, SliderInput, VectorInput } = ReactPCUI;

return fragment(
jsx(
Panel,
{ headerText: 'Attributes' },
jsx(
LabelGroup,
{ text: 'Look sensitivity' },
jsx(SliderInput, {
binding: new BindingTwoWay(),
link: { observer, path: 'attr.lookSensitivity' },
min: 0.1,
max: 1,
step: 0.01
})
),
jsx(
LabelGroup,
{ text: 'Look damping' },
jsx(SliderInput, {
binding: new BindingTwoWay(),
link: { observer, path: 'attr.lookDamping' },
min: 0,
max: 0.999,
step: 0.001,
precision: 3
})
),
jsx(
LabelGroup,
{ text: 'Move damping' },
jsx(SliderInput, {
binding: new BindingTwoWay(),
link: { observer, path: 'attr.moveDamping' },
min: 0,
max: 0.999,
step: 0.001,
precision: 3
})
),
jsx(
LabelGroup,
{ text: 'Pitch range' },
jsx(VectorInput, {
binding: new BindingTwoWay(),
link: { observer, path: 'attr.pitchRange' },
dimensions: 2
})
),
jsx(
LabelGroup,
{ text: 'Move speed' },
jsx(SliderInput, {
binding: new BindingTwoWay(),
link: { observer, path: 'attr.moveSpeed' },
min: 1,
max: 10
})
),
jsx(
LabelGroup,
{ text: 'Sprint speed' },
jsx(SliderInput, {
binding: new BindingTwoWay(),
link: { observer, path: 'attr.sprintSpeed' },
min: 1,
max: 10
})
),
jsx(
LabelGroup,
{ text: 'Crouch speed' },
jsx(SliderInput, {
binding: new BindingTwoWay(),
link: { observer, path: 'attr.crouchSpeed' },
min: 1,
max: 10
})
)
)
);
};
194 changes: 108 additions & 86 deletions examples/src/examples/camera/fly.example.mjs
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
import { deviceType, rootPath } from 'examples/utils';
// @config DESCRIPTION <div style='text-align:center'><div>(<b>WASDQE</b>) Move</div></div>
import { data } from 'examples/observer';
import { deviceType, rootPath, fileImport } from 'examples/utils';
import * as pc from 'playcanvas';

const canvas = /** @type {HTMLCanvasElement} */ (document.getElementById('application-canvas'));
const { CameraControls } = await fileImport(`${rootPath}/static/scripts/camera-controls.mjs`);

const canvas = document.getElementById('application-canvas');
if (!(canvas instanceof HTMLCanvasElement)) {
throw new Error('No canvas found');
}
window.focus();

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

const assets = {
helipad: new pc.Asset(
'helipad-env-atlas',
'texture',
{ url: `${rootPath}/static/assets/cubemaps/helipad-env-atlas.png` },
{ type: pc.TEXTURETYPE_RGBP, mipmaps: false }
),
statue: new pc.Asset('statue', 'container', { url: `${rootPath}/static/assets/models/statue.glb` })
};

const device = await pc.createGraphicsDevice(canvas, gfxOptions);
device.maxPixelRatio = Math.min(window.devicePixelRatio, 2);

const createOptions = new pc.AppOptions();
createOptions.graphicsDevice = device;
createOptions.mouse = new pc.Mouse(document.body);
createOptions.keyboard = new pc.Keyboard(window);

createOptions.componentSystems = [
pc.RenderComponentSystem,
pc.CameraComponentSystem,
pc.LightComponentSystem,
pc.ScriptComponentSystem
];
createOptions.resourceHandlers = [pc.ScriptHandler];
createOptions.resourceHandlers = [pc.TextureHandler, pc.ContainerHandler, pc.ScriptHandler];

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

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

/**
* @param {pc.Asset[] | number[]} assetList - The asset list.
* @param {pc.AssetRegistry} assetRegistry - The asset registry.
* @returns {Promise<void>} The promise.
* Calculate the bounding box of an entity.
*
* @param {pc.BoundingBox} bbox - The bounding box.
* @param {pc.Entity} entity - The entity.
* @returns {pc.BoundingBox} The bounding box.
*/
function loadAssets(assetList, assetRegistry) {
return new Promise((resolve) => {
const assetListLoader = new pc.AssetListLoader(assetList, assetRegistry);
assetListLoader.load(resolve);
const calcEntityAABB = (bbox, entity) => {
bbox.center.set(0, 0, 0);
bbox.halfExtents.set(0, 0, 0);
entity.findComponents('render').forEach((render) => {
render.meshInstances.forEach((/** @type {pc.MeshInstance} */ mi) => {
bbox.add(mi.aabb);
});
});
}
await loadAssets(Object.values(assets), app.assets);
app.scene.ambientLight = new pc.Color(0.2, 0.2, 0.2);
app.start();

// *********** Helper functions *******************
/**
* @param {pc.Color} color - The color.
* @returns {pc.StandardMaterial} The material.
*/
function createMaterial(color) {
const material = new pc.StandardMaterial();
material.diffuse = color;
// we need to call material.update when we change its properties
material.update();
return material;
}
return bbox;
};

/**
* @param {pc.Vec3} position - The position.
* @param {pc.Vec3} size - The size.
* @param {pc.Material} material - The material.
* @param {pc.Entity} focus - The entity to focus the camera on.
* @returns {CameraControls} The camera-controls script.
*/
function createBox(position, size, material) {
// create an entity and add a model component of type 'box'
const box = new pc.Entity();
box.addComponent('render', {
type: 'box',
material: material
const createFlyCamera = (focus) => {
const camera = new pc.Entity();
camera.addComponent('camera');
camera.addComponent('script');
camera.setPosition(0, 20, 30);
app.root.addChild(camera);

const bbox = calcEntityAABB(new pc.BoundingBox(), focus);

/** @type {CameraControls} */
const script = camera.script.create(CameraControls, {
attributes: {
enableOrbit: false,
enablePan: false,
focusPoint: bbox.center,
sceneSize: bbox.halfExtents.length(),
pitchRange: new pc.Vec2(-90, 90)
}
});

// move the box
box.setLocalPosition(position);
box.setLocalScale(size);

// add the box to the hierarchy
app.root.addChild(box);
}

// *********** Create Boxes *******************
return script;
};

// create a few boxes in our scene
const red = createMaterial(pc.Color.RED);
for (let i = 0; i < 3; i++) {
for (let j = 0; j < 2; j++) {
createBox(new pc.Vec3(i * 2, 0, j * 4), pc.Vec3.ONE, red);
}
}
app.start();

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

// *********** Create lights *******************
app.scene.skyboxMip = 1;
app.scene.skyboxIntensity = 0.4;
app.scene.envAtlas = assets.helipad.resource;

// make our scene prettier by adding a directional light
// Create a directional light
const light = new pc.Entity();
light.addComponent('light', {
type: 'omni',
color: new pc.Color(1, 1, 1),
range: 100
});
light.setLocalPosition(0, 0, 2);

// add the light to the hierarchy
light.addComponent('light');
light.setLocalEulerAngles(45, 30, 0);
app.root.addChild(light);

// *********** Create camera *******************

// Create an Entity with a camera component
const camera = new pc.Entity();
camera.addComponent('camera', {
clearColor: new pc.Color(0.5, 0.5, 0.8),
nearClip: 0.3,
farClip: 30
});
const statue = assets.statue.resource.instantiateRenderEntity();
statue.setLocalPosition(0, -0.5, 0);
app.root.addChild(statue);

const multiCameraScript = createFlyCamera(statue);

data.set('attr', [
'lookSensitivity',
'lookDamping',
'moveDamping',
'pitchRange',
'moveSpeed',
'sprintSpeed',
'crouchSpeed'
].reduce((/** @type {Record<string, any>} */ obj, key) => {
const value = multiCameraScript[key];

if (value instanceof pc.Vec2) {
obj[key] = [value.x, value.y];
return obj;
}

// add the fly camera script to the camera
camera.addComponent('script');
camera.script.create('flyCamera');
obj[key] = multiCameraScript[key];
return obj;
}, {}));

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

// Move the camera a little further away
camera.translate(2, 0.8, 9);
if (Array.isArray(value)) {
multiCameraScript[key] = tmpVa.set(value[0], value[1]);
return;
}
if (index !== undefined) {
const arr = data.get(`${category}.${key}`);
multiCameraScript[key] = tmpVa.set(arr[0], arr[1]);
return;
}
multiCameraScript[key] = value;
});

export { app };
Loading
Loading