@playcanvas/react
Version:
A React renderer for PlayCanvas – build interactive 3D applications using React's declarative paradigm.
213 lines (212 loc) • 8.64 kB
JavaScript
import { EnvLighting, Quat, Sky, SKYTYPE_DOME, SKYTYPE_INFINITE } from "playcanvas";
import { useEffect, useRef } from "react";
import { useApp } from "../hooks/use-app.js";
import { createComponentDefinition, getStaticNullApplication, validatePropsWithDefaults, warnOnce } from "../utils/validation.js";
import { Asset } from "playcanvas";
import dedent from "dedent";
const appUUIDs = new Set();
/**
* @beta
* Environment component
* The Environment component is used to set the environment lighting and skybox.
*
* @example
* ```tsx
* <Environment />
* ```
*/
function Environment(props) {
const app = useApp();
// split the sky props and scene props
const { center, scale, rotation, depthWrite, type, showSkybox, ...sceneProps } = props;
const skyProps = { center, scale, rotation, depthWrite, type, showSkybox };
// Sanitize and validate the props
const safeSceneProps = validatePropsWithDefaults(sceneProps, sceneComponentDefinition);
const safeSkyProps = validatePropsWithDefaults(skyProps, skyComponentDefinition);
/**
* We want to ensure that the environment is only set once per app instance.
* This is because the environment is a global state and we don't want to
* set it multiple times.
*
* If multiple components are used in the same app instance, we will warn the user
* and only the first component will be used.
*/
const appHasEnvironment = useRef(false);
useEffect(() => {
const appUUID = app.root.getGuid();
const hasEnvironment = appUUIDs.has(appUUID);
appUUIDs.add(appUUID);
appHasEnvironment.current = hasEnvironment;
if (hasEnvironment) {
warnOnce(dedent `Multiple \`<Environment/>\` components have been mounted.
Only the first \`<Environment/>\` component will be used.`);
}
return () => {
appUUIDs.delete(appUUID);
};
});
/**
* Sets the skybox of the environment.
*/
useEffect(() => {
// If the app already has an environment, don't override it
if (appHasEnvironment.current)
return;
const skyBoxAsset = safeSceneProps.skybox;
if (!skyBoxAsset)
return;
const isCubeMap = Array.isArray(skyBoxAsset.resources) && skyBoxAsset.resources.length === 6;
let skybox = skyBoxAsset.resource;
// If the skybox is not a cube map, try to generate a cube map from it.
if (!isCubeMap) {
skybox = EnvLighting.generateSkyboxCubemap(skyBoxAsset.resource);
}
app.scene.skybox = skybox;
return () => {
if (app?.scene) {
app.scene.skybox = null;
}
};
}, [appHasEnvironment.current, safeSceneProps.skybox?.id]);
/**
* Sets the environment lighting.
*/
useEffect(() => {
// If the app already has an environment, don't override it
if (appHasEnvironment.current)
return;
app.scene.envAtlas = safeSceneProps?.envAtlas?.resource ?? null;
return () => {
if (app?.scene) {
app.scene.envAtlas = null;
}
};
}, [appHasEnvironment.current, safeSceneProps.envAtlas?.id]);
/**
* Sets the remaining environment settings.
*/
useEffect(() => {
if (appHasEnvironment.current)
return;
app.scene.exposure = safeSceneProps.exposure ?? 1;
app.scene.envAtlas = safeSceneProps.envAtlas?.resource ?? null;
if (safeSkyProps.rotation) {
app.scene.skyboxRotation = new Quat().setFromEulerAngles(safeSkyProps.rotation[0], safeSkyProps.rotation[1], safeSkyProps.rotation[2]);
}
if (safeSkyProps.scale) {
app.scene.sky.node.setLocalScale(...safeSkyProps.scale);
}
if (safeSkyProps.position) {
app.scene.sky.node.setLocalPosition(...safeSkyProps.position);
}
if (safeSkyProps.center) {
app.scene.sky.center.set(...safeSkyProps.center);
}
app.scene.sky.type = safeSkyProps.type ?? SKYTYPE_DOME;
app.scene.sky.depthWrite = safeSkyProps.depthWrite ?? true;
// Set the skybox mip level
app.scene.skyboxMip = safeSceneProps.skyboxMip ?? 0;
app.scene.skyboxLuminance = safeSceneProps.skyboxLuminance ?? 1;
app.scene.skyboxIntensity = safeSceneProps.skyboxIntensity ?? 1;
app.scene.skyboxHighlightMultiplier = safeSceneProps.skyboxHighlightMultiplier ?? 1;
const layer = app?.scene?.layers?.getLayerByName('Skybox');
if (layer) {
layer.enabled = safeSkyProps.showSkybox ?? true;
}
return () => {
/**
* We have hardcoded the default values for the scene and sky in order to reset them
*
* This isn't perfect as any changes the the engine defaults will break this.
* TODO: Find a better way to reset the scene and sky.
*/
if (app.scene) {
app.scene.exposure = 1;
app.scene.skyboxRotation = new Quat().setFromEulerAngles(0, 0, 0);
app.scene.sky.node.setLocalScale(1, 1, 1);
app.scene.sky.node.setLocalPosition(0, 0, 0);
app.scene.sky.center.set(0, 0.05, 0);
app.scene.sky.type = SKYTYPE_INFINITE;
app.scene.sky.depthWrite = false;
app.scene.skyboxMip = 0;
app.scene.skyboxLuminance = 0;
app.scene.skyboxIntensity = 1;
app.scene.skyboxHighlightMultiplier = 1;
const layer = app?.scene?.layers?.getLayerByName('Skybox');
if (layer) {
layer.enabled = true;
}
}
};
}, [
appHasEnvironment.current,
safeSceneProps.exposure,
safeSkyProps.type,
safeSkyProps.depthWrite,
safeSkyProps.showSkybox,
safeSceneProps.skyboxMip,
safeSceneProps.skyboxLuminance,
safeSceneProps.skyboxIntensity,
safeSceneProps.skyboxHighlightMultiplier,
// compute keys for scale, rotation, and center
`scale-${safeSkyProps.scale?.join('-')}`,
`rotation-${safeSkyProps.rotation?.join('-')}`,
`center-${safeSkyProps.center?.join('-')}`,
]);
return null;
}
;
// Component Definitions
const skyComponentDefinition = createComponentDefinition("Sky", () => new Sky(getStaticNullApplication().scene), (sky) => sky.resetSkyMesh());
const sceneComponentDefinition = createComponentDefinition("Scene", () => getStaticNullApplication().scene);
skyComponentDefinition.schema = {
...skyComponentDefinition.schema,
scale: {
validate: (value) => Array.isArray(value)
&& value.length === 3
&& value.every(v => typeof v === 'number'),
errorMsg: (value) => `Expected an array of 3 numbers, got \`${typeof value}\``,
default: [100, 100, 100],
},
rotation: {
validate: (value) => Array.isArray(value)
&& value.length === 3
&& value.every(v => typeof v === 'number'),
errorMsg: (value) => `Expected an array of 3 numbers, got \`${typeof value}\``,
default: [0, 0, 0],
},
position: {
validate: (value) => Array.isArray(value)
&& value.length === 3
&& value.every(v => typeof v === 'number'),
errorMsg: (value) => `Expected an array of 3 numbers, got \`${typeof value}\``,
default: [0, 0, 0],
},
center: {
validate: (value) => Array.isArray(value)
&& value.length === 3
&& value.every(v => typeof v === 'number'),
errorMsg: (value) => `Expected an array of 3 numbers, got \`${typeof value}\``,
default: [0, 0.05, 0],
},
showSkybox: {
validate: (value) => typeof value === "boolean",
errorMsg: (value) => `Expected a boolean, got \`${typeof value}\``,
default: true,
}
};
sceneComponentDefinition.schema = {
...sceneComponentDefinition.schema,
envAtlas: {
validate: (value) => value instanceof Asset && value.type === 'texture',
errorMsg: (value) => `Expected a \`Asset\` instance, got \`${typeof value}\``,
default: null,
},
skybox: {
validate: (value) => value instanceof Asset && value.type === 'texture',
errorMsg: (value) => `Expected a \`Asset\` instance, got \`${typeof value}\``,
default: null,
},
};
export { Environment };
//# sourceMappingURL=Environment.js.map