Skip to content

Commit

Permalink
Merge branch 'main' into xr-camera-controls
Browse files Browse the repository at this point in the history
  • Loading branch information
willeastcott authored Nov 29, 2024
2 parents ee9d943 + 17de544 commit 60cd49d
Show file tree
Hide file tree
Showing 17 changed files with 523 additions and 162 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ pc.WasmModule.setConfig('DracoDecoderModule', {
const assets = {
laboratory: new pc.Asset('statue', 'container', { url: `${rootPath}/static/assets/models/laboratory.glb` }),
orbit: new pc.Asset('orbit', 'script', { url: `${rootPath}/static/scripts/camera/orbit-camera.js` }),
ssao: new pc.Asset('ssao', 'script', { url: `${rootPath}/static/scripts/posteffects/posteffect-ssao.js` }),
helipad: new pc.Asset(
'helipad-env-atlas',
'texture',
Expand Down
101 changes: 101 additions & 0 deletions scripts/esm/xr-controllers.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
/* eslint-disable-next-line import/no-unresolved */
import { Script } from 'playcanvas';

export default class XrControllers extends Script {
/**
* The base URL for fetching the WebXR input profiles.
*
* @attribute
* @type {string}
*/
basePath = 'https://cdn.jsdelivr.net/npm/@webxr-input-profiles/assets/dist/profiles';

controllers = new Map();

initialize() {
this.app.xr.input.on('add', async (inputSource) => {
if (!inputSource.profiles?.length) {
console.warn('No profiles available for input source');
return;
}

// Process all profiles concurrently
const profilePromises = inputSource.profiles.map(async (profileId) => {
const profileUrl = `${this.basePath}/${profileId}/profile.json`;

try {
const response = await fetch(profileUrl);
if (!response.ok) {
return null;
}

const profile = await response.json();
const layoutPath = profile.layouts[inputSource.handedness]?.assetPath || '';
const assetPath = `${this.basePath}/${profile.profileId}/${inputSource.handedness}${layoutPath.replace(/^\/?(left|right)/, '')}`;

// Load the model
const asset = await new Promise((resolve, reject) => {
this.app.assets.loadFromUrl(assetPath, 'container', (err, asset) => {
if (err) reject(err);
else resolve(asset);
});
});

return { profileId, asset };
} catch (error) {
console.warn(`Failed to process profile ${profileId}`);
return null;
}
});

// Wait for all profile attempts to complete
const results = await Promise.all(profilePromises);
const successfulResult = results.find(result => result !== null);

if (successfulResult) {
const { asset } = successfulResult;
const container = asset.resource;
const entity = container.instantiateRenderEntity();
this.app.root.addChild(entity);

const jointMap = new Map();
if (inputSource.hand) {
for (const joint of inputSource.hand.joints) {
const jointEntity = entity.findByName(joint.id);
if (jointEntity) {
jointMap.set(joint, jointEntity);
}
}
}

this.controllers.set(inputSource, { entity, jointMap });
} else {
console.warn('No compatible profiles found');
}
});

this.app.xr.input.on('remove', (inputSource) => {
const controller = this.controllers.get(inputSource);
if (controller) {
controller.entity.destroy();
this.controllers.delete(inputSource);
}
});
}

update(dt) {
if (this.app.xr.active) {
for (const [inputSource, { entity, jointMap }] of this.controllers) {
if (inputSource.hand) {
for (const [joint, jointEntity] of jointMap) {
jointEntity.setPosition(joint.getPosition());
jointEntity.setRotation(joint.getRotation());
}
} else {
entity.setPosition(inputSource.getPosition());
entity.setRotation(inputSource.getRotation());
}
}
}
}
}
36 changes: 24 additions & 12 deletions scripts/utils/cubemap-renderer.js
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,8 @@ CubemapRenderer.prototype.initialize = function () {
];

// set up rendering for all 6 faces
let firstCamera = null;
let lastCamera = null;
for (var i = 0; i < 6; i++) {

// render target, connected to cubemap texture face
Expand Down Expand Up @@ -109,19 +111,29 @@ CubemapRenderer.prototype.initialize = function () {
// set up its rotation
e.setRotation(cameraRotations[i]);

// Before the first camera renders, trigger onCubemapPreRender event on the entity.
if (i === 0) {
e.camera.onPreRender = () => {
this.entity.fire('onCubemapPreRender');
};
// keep the first and last camera
if (i === 0) firstCamera = e.camera;
if (i === 5) lastCamera = e.camera;
}

// Before the first camera renders, trigger onCubemapPreRender event on the entity.
this.evtPreRender = this.app.scene.on('prerender', (cameraComponent) => {
if (cameraComponent === firstCamera) {
this.entity.fire('onCubemapPreRender');
}
});

// When last camera is finished rendering, trigger onCubemapPostRender event on the entity.
// This can be listened to by the user, and the resulting cubemap can be further processed (e.g prefiltered)
if (i === 5) {
e.camera.onPostRender = () => {
this.entity.fire('onCubemapPostRender');
};
// When last camera is finished rendering, trigger onCubemapPostRender event on the entity.
// This can be listened to by the user, and the resulting cubemap can be further processed (e.g pre-filtering)
this.evtPostRender = this.app.scene.on('postrender', (cameraComponent) => {
if (cameraComponent === lastCamera) {
this.entity.fire('onCubemapPostRender');
}
}
});

// when the script is destroyed, remove event listeners
this.on('destroy', () => {
this.evtPreRender.off();
this.evtPostRender.off();
});
};
96 changes: 96 additions & 0 deletions scripts/utils/mac-gpu-profiling.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
/**
* This script allows GPU Profiling on Mac using Xcode's GPU Frame Capture. Please read the instructions
* in the manual: https://developer.playcanvas.com/user-manual/optimization/gpu-profiling/
*/
var MacGPUProfiling = pc.createScript('MacGPUProfiling');

// Called once after all resources are loaded and initialized
MacGPUProfiling.prototype.initialize = function () {
this.isInitialized = false;
this.device = null;
this.context = null;

// this is not needed for WebGPU
if (this.app.graphicsDevice.isWebGPU) return;

// only needed on Mac
if (pc.platform.name !== 'osx') return;

// Create a new canvas for WebGPU with a smaller size
this.webgpuCanvas = document.createElement('canvas');
this.webgpuCanvas.width = 20;
this.webgpuCanvas.height = 20;
this.webgpuCanvas.style.position = 'absolute';
this.webgpuCanvas.style.top = '20px'; // Adjust position if needed
this.webgpuCanvas.style.left = '20px'; // Adjust position if needed
document.body.appendChild(this.webgpuCanvas);

// Start the asynchronous WebGPU initialization
this.initWebGPU();
};

// Async function for WebGPU initialization
MacGPUProfiling.prototype.initWebGPU = async function () {
// Check for WebGPU support
if (!navigator.gpu) {
console.error('WebGPU is not supported on this browser.');
return;
}

// Get WebGPU adapter and device
const adapter = await navigator.gpu.requestAdapter();
this.device = await adapter.requestDevice();

console.log('Created WebGPU device used for profiling');

// Create a WebGPU context for the new canvas
this.context = this.webgpuCanvas.getContext('webgpu');

// Configure the WebGPU context
const swapChainFormat = 'bgra8unorm';
this.context.configure({
device: this.device,
format: swapChainFormat
});

// Mark initialization as complete
this.isInitialized = true;

// Hook into the 'frameend' event
this.app.on('frameend', this.onFrameEnd, this);
};

// Called when the 'frameend' event is triggered
MacGPUProfiling.prototype.onFrameEnd = function () {
// If WebGPU is not initialized yet, do nothing
if (!this.isInitialized) return;

// Clear the WebGPU surface to red after WebGL rendering
this.clearToRed();
};

// Function to clear the WebGPU surface to red
MacGPUProfiling.prototype.clearToRed = function () {
// Get the current texture to render to
const textureView = this.context.getCurrentTexture().createView();

// Create a command encoder
const commandEncoder = this.device.createCommandEncoder();

// Create a render pass descriptor with a red background
const renderPassDescriptor = {
colorAttachments: [{
view: textureView,
clearValue: { r: 1.0, g: 0.0, b: 0.0, a: 1.0 }, // Red background
loadOp: 'clear',
storeOp: 'store'
}]
};

// render pass
const passEncoder = commandEncoder.beginRenderPass(renderPassDescriptor);
passEncoder.end();

// Submit the commands to the GPU
this.device.queue.submit([commandEncoder.finish()]);
};
13 changes: 10 additions & 3 deletions scripts/utils/planar-renderer.js
Original file line number Diff line number Diff line change
Expand Up @@ -68,9 +68,16 @@ PlanarRenderer.prototype.initialize = function () {

// When the camera is finished rendering, trigger onPlanarPostRender event on the entity.
// This can be listened to by the user, and the resulting texture can be further processed (e.g prefiltered)
planarCamera.onPostRender = () => {
this.entity.fire('onPlanarPostRender');
};
this.evtPostRender = this.app.scene.on('postrender', (cameraComponent) => {
if (planarCamera === cameraComponent) {
this.entity.fire('onPlanarPostRender');
}
});

// when the script is destroyed, remove event listeners
this.on('destroy', () => {
this.evtPostRender.off();
});
};

PlanarRenderer.prototype.updateRenderTarget = function () {
Expand Down
4 changes: 2 additions & 2 deletions src/core/event-handle.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ class EventHandle {

/**
* @type {string}
* @private
* @ignore
*/
name;

Expand Down Expand Up @@ -88,7 +88,7 @@ class EventHandle {
*/
off() {
if (this._removed) return;
this.handler.off(this.name, this.callback, this.scope);
this.handler.offByHandle(this);
}

on(name, callback, scope = this) {
Expand Down
34 changes: 34 additions & 0 deletions src/core/event-handler.js
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,40 @@ class EventHandler {
return this;
}

/**
* Detach an event handler from an event using EventHandle instance. More optimal remove
* as it does not have to scan callbacks array.
*
* @param {EventHandle} handle - Handle of event.
* @ignore
*/
offByHandle(handle) {
const name = handle.name;
handle.removed = true;

// if we are removing a callback from the list that is executing right now
// ensure we preserve initial list before modifications
if (this._callbackActive.has(name) && this._callbackActive.get(name) === this._callbacks.get(name)) {
this._callbackActive.set(name, this._callbackActive.get(name).slice());
}

const callbacks = this._callbacks.get(name);
if (!callbacks) {
return this;
}

const ind = callbacks.indexOf(handle);
if (ind !== -1) {
callbacks.splice(ind, 1);

if (callbacks.length === 0) {
this._callbacks.delete(name);
}
}

return this;
}

/**
* Fire an event, all additional arguments are passed on to the event listener.
*
Expand Down
40 changes: 24 additions & 16 deletions src/deprecated/deprecated.js
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ import { RigidBodyComponent } from '../framework/components/rigid-body/component
import { RigidBodyComponentSystem } from '../framework/components/rigid-body/system.js';
import { LitShader } from '../scene/shader-lib/programs/lit-shader.js';
import { Geometry } from '../scene/geometry/geometry.js';
import { CameraComponent } from '../framework/components/camera/component.js';

// MATH

Expand Down Expand Up @@ -564,30 +565,37 @@ Object.defineProperty(Scene.prototype, 'models', {
}
});

// A helper function to add deprecated set and get property on a Layer
function _removedLayerProperty(name) {
Object.defineProperty(Layer.prototype, name, {
// A helper function to add deprecated set and get property on a class
function _removedClassProperty(targetClass, name, comment = '') {
Object.defineProperty(targetClass.prototype, name, {
set: function (value) {
Debug.errorOnce(`pc.Layer#${name} has been removed.`);
Debug.errorOnce(`${targetClass.name}#${name} has been removed. ${comment}`);
},
get: function () {
Debug.errorOnce(`pc.Layer#${name} has been removed.`);
Debug.errorOnce(`${targetClass.name}#${name} has been removed. ${comment}`);
return undefined;
}
});
}

_removedLayerProperty('renderTarget');
_removedLayerProperty('onPreCull');
_removedLayerProperty('onPreRender');
_removedLayerProperty('onPreRenderOpaque');
_removedLayerProperty('onPreRenderTransparent');
_removedLayerProperty('onPostCull');
_removedLayerProperty('onPostRender');
_removedLayerProperty('onPostRenderOpaque');
_removedLayerProperty('onPostRenderTransparent');
_removedLayerProperty('onDrawCall');
_removedLayerProperty('layerReference');
_removedClassProperty(Layer, 'renderTarget');
_removedClassProperty(Layer, 'onPreCull');
_removedClassProperty(Layer, 'onPreRender');
_removedClassProperty(Layer, 'onPreRenderOpaque');
_removedClassProperty(Layer, 'onPreRenderTransparent');
_removedClassProperty(Layer, 'onPostCull');
_removedClassProperty(Layer, 'onPostRender');
_removedClassProperty(Layer, 'onPostRenderOpaque');
_removedClassProperty(Layer, 'onPostRenderTransparent');
_removedClassProperty(Layer, 'onDrawCall');
_removedClassProperty(Layer, 'layerReference');

_removedClassProperty(CameraComponent, 'onPreCull', 'Use Scene#EVENT_PRECULL event instead.');
_removedClassProperty(CameraComponent, 'onPostCull', 'Use Scene#EVENT_POSTCULL event instead.');
_removedClassProperty(CameraComponent, 'onPreRender', 'Use Scene#EVENT_PRERENDER event instead.');
_removedClassProperty(CameraComponent, 'onPostRender', 'Use Scene#EVENT_POSTRENDER event instead.');
_removedClassProperty(CameraComponent, 'onPreRenderLayer', 'Use Scene#EVENT_PRERENDER_LAYER event instead.');
_removedClassProperty(CameraComponent, 'onPostRenderLayer', 'Use Scene#EVENT_POSTRENDER_LAYER event instead.');

ForwardRenderer.prototype.renderComposition = function (comp) {
Debug.deprecated('pc.ForwardRenderer#renderComposition is deprecated. Use pc.AppBase.renderComposition instead.');
Expand Down
Loading

0 comments on commit 60cd49d

Please sign in to comment.