Skip to content

Commit

Permalink
Physically based light units (#4618)
Browse files Browse the repository at this point in the history
* Add luminance to light.

Use luminance to calculate final light contribution before passing intensity and color to shader.

* Create example showing off physically based light and camera units

* Fix types.

* Changed name of example

* Calculate spot light intensity as a function of inner and outer cone angles.

* Fix model to render.

* Clarified light luminance calculation code

Added documentation references to the calculation for the lux calculation for the light sources.
Changed from an if chain to switch to make it more clear the last condition is for directional lights.

* Fix lint

* Make sure inner angle also updates intensity.

* Add internal flag to make sure lights calculate physical units

If the scene changes the physical units flag after the light is added, the lights will need their final color calculated to reflect that.

* Fix lights physical units toggle

* Flip the physical light switch only on visible lights

* Realised the sky intensity wasn't correctly handled as a separate unit.

* Added toggle for physical light units to example

* Fix compile issue

* Move accessor out of inner loop.

* Add support for physically based luminance for ambient light

* Add comment for skyboxLuminance

* Fix doc.

* Add missing ambient light doc and apply.

* Fix ambient luminance copy.

* Add switch for toggling skybox on/off (and using ambient light instead)

* Fix luts

* Added ambient light which will replace the sky when disabled.

* Added comment about light luminance in component.

* Update src/scene/scene.js

Co-authored-by: Will Eastcott <will@playcanvas.com>

* Update src/scene/scene.js

Co-authored-by: Will Eastcott <will@playcanvas.com>

Co-authored-by: Martin Valigursky <59932779+mvaligursky@users.noreply.github.com>
Co-authored-by: Will Eastcott <will@playcanvas.com>
  • Loading branch information
3 people authored Sep 23, 2022
1 parent 637e2f0 commit 1da43a4
Show file tree
Hide file tree
Showing 11 changed files with 431 additions and 8 deletions.
Binary file added examples/assets/models/Lights.glb
Binary file not shown.
2 changes: 2 additions & 0 deletions examples/src/examples/graphics/index.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import LayersExample from "./layers";
import LightsBakedAOExample from "./lights-baked-a-o";
import LightsBakedExample from "./lights-baked";
import LightsExample from "./lights";
import LightPhysicalUnitsExample from "./light-physical-units";
import LinesExample from "./lines";
import MaterialAnisotropicExample from "./material-anisotropic";
import MaterialBasicExample from "./material-basic";
Expand Down Expand Up @@ -69,6 +70,7 @@ export {
LightsBakedAOExample,
LightsBakedExample,
LightsExample,
LightPhysicalUnitsExample,
LinesExample,
MaterialAnisotropicExample,
MaterialBasicExample,
Expand Down
326 changes: 326 additions & 0 deletions examples/src/examples/graphics/light-physical-units.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,326 @@
import React from 'react';
import * as pc from '../../../../';

import { BindingTwoWay } from '@playcanvas/pcui';
import { BooleanInput, LabelGroup, Panel, SliderInput } from '@playcanvas/pcui/react';
import { Observer } from '@playcanvas/observer';
class LightPhysicalUnitsExample {
static CATEGORY = 'Graphics';
static NAME = 'Light Physical Units';

controls(data: Observer) {
return <>
<Panel headerText='Lights'>
<LabelGroup text='Sun (lm/m2)'>
<SliderInput binding={new BindingTwoWay()} link={{ observer: data, path: 'script.sun.luminance' }} min={0.0} max={100000.0}/>
</LabelGroup>
<LabelGroup text='Sky (lm/m2)'>
<SliderInput binding={new BindingTwoWay()} link={{ observer: data, path: 'script.sky.luminance' }} min={0.0} max={100000.0}/>
</LabelGroup>
<LabelGroup text='Spot (lm)'>
<SliderInput binding={new BindingTwoWay()} link={{ observer: data, path: 'script.spot.luminance' }} min={0.0} max={200000.0}/>
</LabelGroup>
<LabelGroup text='Spot aperture'>
<SliderInput binding={new BindingTwoWay()} link={{ observer: data, path: 'script.spot.aperture' }} min={1.0} max={90.0}/>
</LabelGroup>
<LabelGroup text='Point (lm)'>
<SliderInput binding={new BindingTwoWay()} link={{ observer: data, path: 'script.point.luminance' }} min={0.0} max={800000.0}/>
</LabelGroup>
<LabelGroup text='Rect (lm)'>
<SliderInput binding={new BindingTwoWay()} link={{ observer: data, path: 'script.rect.luminance' }} min={0.0} max={800000.0}/>
</LabelGroup>
</Panel>
<Panel headerText='Camera'>
<LabelGroup text='Aperture (f-stop)'>
<SliderInput binding={new BindingTwoWay()} link={{ observer: data, path: 'script.camera.aperture' }} min={1.0} max={16.0}/>
</LabelGroup>
<LabelGroup text='Shutter Speed (1/x)'>
<SliderInput binding={new BindingTwoWay()} link={{ observer: data, path: 'script.camera.shutter' }} min={1.0} max={1000.0}/>
</LabelGroup>
<LabelGroup text='Sensitivity (ISO)'>
<SliderInput binding={new BindingTwoWay()} link={{ observer: data, path: 'script.camera.sensitivity' }} min={100.0} max={1000.0}/>
</LabelGroup>
<LabelGroup text='Animate'>
<BooleanInput binding={new BindingTwoWay()} link={{ observer: data, path: 'script.camera.animate' }}/>
</LabelGroup>
</Panel>
<Panel headerText='Scene'>
<LabelGroup text='Toggle physical light and camera units'>
<BooleanInput binding={new BindingTwoWay()} link={{ observer: data, path: 'script.scene.physicalUnits' }}/>
</LabelGroup>
<LabelGroup text='Toggle sky'>
<BooleanInput binding={new BindingTwoWay()} link={{ observer: data, path: 'script.scene.sky' }}/>
</LabelGroup>
</Panel>
</>;
}

example(canvas: HTMLCanvasElement, data: any): void {

// Create the app and start the update loop
const app = new pc.Application(canvas, {
graphicsDeviceOptions: {
alpha: true
},
mouse: new pc.Mouse(document.body),
keyboard: new pc.Keyboard(document.body),
touch: new pc.TouchDevice(document.body)
});

const assets = {
orbitCamera: new pc.Asset('script', 'script', { url: '/static/scripts/camera/orbit-camera.js' }),
helipad: new pc.Asset('helipad.dds', 'cubemap', { url: '/static/assets/cubemaps/helipad.dds' }, { type: pc.TEXTURETYPE_RGBM }),
lights: new pc.Asset('lights', 'container', { url: '/static/assets/models/Lights.glb' }),
sheen: new pc.Asset('sheen', 'container', { url: '/static/assets/models/SheenChair.glb' }),
color: new pc.Asset('color', 'texture', { url: '/static/assets/textures/seaside-rocks01-color.jpg' }),
normal: new pc.Asset('normal', 'texture', { url: '/static/assets/textures/seaside-rocks01-normal.jpg' }),
gloss: new pc.Asset('gloss', 'texture', { url: '/static/assets/textures/seaside-rocks01-gloss.jpg' }),
luts: new pc.Asset('luts', 'json', { url: '/static/assets/json/area-light-luts.json' })
};

const assetListLoader = new pc.AssetListLoader(Object.values(assets), app.assets);
assetListLoader.load(() => {
// Set the canvas to fill the window and automatically change resolution to be the same as the canvas size
app.setCanvasFillMode(pc.FILLMODE_FILL_WINDOW);
app.setCanvasResolution(pc.RESOLUTION_AUTO);

app.start();

app.scene.toneMapping = pc.TONEMAP_ACES;
app.scene.skyboxMip = 1;
app.scene.ambientLight.set(1, 0, 0);
app.scene.ambientLuminance = 20000;

// set the loaded area light LUT data
const luts = assets.luts.resource;
app.setAreaLightLuts(luts.LTC_MAT_1, luts.LTC_MAT_2);

const sheen1 = assets.sheen.resource.instantiateRenderEntity({
castShadows: true
});
sheen1.setLocalScale(new pc.Vec3(3, 3, 3));
sheen1.setLocalPosition(7, -1.0, 0);
app.root.addChild(sheen1);

const sheen2 = assets.sheen.resource.instantiateRenderEntity({
castShadows: true
});
sheen2.setLocalScale(new pc.Vec3(3, 3, 3));
sheen2.setLocalPosition(4, -1.0, 0);
assets.sheen.resource.applyMaterialVariant(sheen2, "Peacock Velvet");
app.root.addChild(sheen2);

const lights = assets.lights.resource.instantiateRenderEntity({
castShadows: true
});
// enable all lights from the glb
const lightComponents: Array<pc.LightComponent> = lights.findComponents("light");
lightComponents.forEach((component) => {
component.enabled = true;
});
lights.setLocalPosition(10, 0, 0);
app.root.addChild(lights);

const material = new pc.StandardMaterial();
material.diffuseMap = assets.color.resource;
material.normalMap = assets.normal.resource;
material.glossMap = assets.gloss.resource;
material.metalness = 0.7;
material.shininess = 80;
material.useMetalness = true;

material.diffuseMapTiling.set(17, 17);
material.normalMapTiling.set(17, 17);
material.glossMapTiling.set(17, 17);
material.update();

const plane = new pc.Entity();
plane.addComponent('render', {
type: 'plane',
material: material
});
plane.setLocalScale(new pc.Vec3(100, 0, 100));
plane.setLocalPosition(0, -1.0, 0);
app.root.addChild(plane);


data.set('script', {
sun: {
luminance: 100000
},
sky: {
luminance: 20000
},
spot: {
luminance: 200000,
aperture: 45
},
point: {
luminance: 100000
},
rect: {
luminance: 100000
},
camera: {
aperture: 16.0,
shutter: 1000,
sensitivity: 1000,
animate: false
},
scene: {
physicalUnits: true,
sky: true
}
});

app.scene.physicalUnits = data.get('script.scene.physicalUnits');
app.scene.setSkybox(assets.helipad.resources);

// Create an Entity with a camera component
const camera = new pc.Entity();
camera.addComponent("camera", {
clearColor: new pc.Color(0.4, 0.45, 0.5),
aperture: data.get('script.camera.aperture'),
shutter: 1 / data.get('script.camera.shutter'),
sensitivity: data.get('script.camera.sensitivity')
});
camera.setLocalPosition(0, 5, 30);

camera.camera.requestSceneColorMap(true);
camera.addComponent("script");
camera.script.create("orbitCamera", {
attributes: {
inertiaFactor: 0.2,
focusEntity: sheen1,
distanceMin: 1,
distanceMax: 400,
frameOnStart: false
}
});
camera.script.create("orbitCameraInputMouse");
camera.script.create("orbitCameraInputTouch");
app.root.addChild(camera);

app.scene.skyboxLuminance = data.get('script.sky.luminance');


const directionalLight = new pc.Entity();
directionalLight.addComponent("light", {
type: "directional",
color: pc.Color.WHITE,
castShadows: true,
luminance: data.get('script.sun.luminance'),
shadowBias: 0.2,
normalOffsetBias: 0.05,
shadowResolution: 2048
});
directionalLight.setEulerAngles(45, 35, 0);
app.root.addChild(directionalLight);

const pointLight = new pc.Entity();
pointLight.addComponent("light", {
type: "point",
color: pc.Color.WHITE,
castShadows: false,
luminance: data.get('script.point.luminance'),
shadowBias: 0.2,
normalOffsetBias: 0.05,
shadowResolution: 2048
});
pointLight.setLocalPosition(0, 5, 0);
app.root.addChild(pointLight);

const spotLight = new pc.Entity();
spotLight.addComponent("light", {
type: "spot",
color: pc.Color.WHITE,
castShadows: false,
luminance: data.get('script.spot.luminance'),
shadowBias: 0.2,
normalOffsetBias: 0.05,
shadowResolution: 2048,
outerConeAngle: data.get('script.spot.aperture'),
innerConeAngle: 0
});
spotLight.setEulerAngles(0, 0, 0);
spotLight.setLocalPosition(10, 5, 5);
app.root.addChild(spotLight);

const areaLight = new pc.Entity();
areaLight.addComponent("light", {
type: "spot",
shape: pc.LIGHTSHAPE_RECT,
color: pc.Color.WHITE,
range: 9999,
luminance: data.get('script.rect.luminance'),
falloffMode: pc.LIGHTFALLOFF_INVERSESQUARED,
innerConeAngle: 80,
outerConeAngle: 85,
normalOffsetBias: 0.1
});
areaLight.setLocalScale(2, 2, 2);
areaLight.setEulerAngles(90, 180, 0);
areaLight.setLocalPosition(5, 0, -5);

// emissive material that is the light source color
const brightMaterial = new pc.StandardMaterial();
brightMaterial.emissive = pc.Color.WHITE;
brightMaterial.emissiveIntensity = areaLight.light.luminance;
brightMaterial.useLighting = false;
brightMaterial.cull = pc.CULLFACE_NONE;
brightMaterial.update();

const brightShape = new pc.Entity();
// primitive shape that matches light source shape
brightShape.addComponent("render", {
type: "plane",
material: brightMaterial,
castShadows: false
});
areaLight.addChild(brightShape);
app.root.addChild(areaLight);

data.on('*:set', (path: string, value: any) => {
if (path === 'script.sun.luminance') {
directionalLight.light.luminance = value;
} else if (path === 'script.sky.luminance') {
app.scene.skyboxLuminance = value;
} else if (path === 'script.spot.luminance') {
spotLight.light.luminance = value;
} else if (path === 'script.spot.aperture') {
spotLight.light.outerConeAngle = value;
} else if (path === 'script.point.luminance') {
pointLight.light.luminance = value;
} else if (path === 'script.rect.luminance') {
areaLight.light.luminance = value;
brightMaterial.emissiveIntensity = value;
brightMaterial.update();
} else if (path === 'script.camera.aperture') {
camera.camera.aperture = value;
} else if (path === 'script.camera.shutter') {
camera.camera.shutter = 1 / value;
} else if (path === 'script.camera.sensitivity') {
camera.camera.sensitivity = value;
} else if (path === 'script.scene.physicalUnits') {
app.scene.physicalUnits = value;
} else if (path === 'script.scene.sky') {
if (value) {
app.scene.setSkybox(assets.helipad.resources);
} else {
app.scene.setSkybox(null);
}
}
});

let time = 0;
app.on("update", function (dt) {
time += dt;
if (data.get('script.camera.animate')) {
data.set('script.camera.aperture', 3 + (1 + Math.sin(time)) * 5.0);
}
});
});
}
}

export default LightPhysicalUnitsExample;
2 changes: 2 additions & 0 deletions src/framework/app-base.js
Original file line number Diff line number Diff line change
Expand Up @@ -1528,6 +1528,7 @@ class AppBase extends EventHandler {
* @param {number|null} [settings.render.skybox] - The asset ID of the cube map texture to be
* used as the scene's skybox. Defaults to null.
* @param {number} settings.render.skyboxIntensity - Multiplier for skybox intensity.
* @param {number} settings.render.skyboxLuminance - Lux (lm/m^2) value for skybox intensity when physical light units are enabled.
* @param {number} settings.render.skyboxMip - The mip level of the skybox to be displayed.
* Only valid for prefiltered cubemap skyboxes.
* @param {number[]} settings.render.skyboxRotation - Rotation of skybox.
Expand All @@ -1543,6 +1544,7 @@ class AppBase extends EventHandler {
* @param {number} settings.render.ambientBakeSpherePart - How much of the sphere to include when baking ambient light.
* @param {number} settings.render.ambientBakeOcclusionBrightness - Brighness of the baked ambient occlusion.
* @param {number} settings.render.ambientBakeOcclusionContrast - Contrast of the baked ambient occlusion.
* @param {number} settings.render.ambientLuminance - Lux (lm/m^2) value for ambient light intensity.
*
* @param {boolean} settings.render.clusteredLightingEnabled - Enable clustered lighting.
* @param {boolean} settings.render.lightingShadowsEnabled - If set to true, the clustered lighting will support shadows.
Expand Down
4 changes: 4 additions & 0 deletions src/framework/components/light/component.js
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ const _lightPropsDefault = [];
* @property {Color} color The Color of the light. The alpha component of the color is ignored.
* Defaults to white (1, 1, 1).
* @property {number} intensity The brightness of the light. Defaults to 1.
* @property {number} luminance The physically based luminance. Only used if scene.physicalUnits is true. Defaults to 0.
* @property {number} shape The light source shape. Can be:
*
* - {@link pc.LIGHTSHAPE_PUNCTUAL}: Infinitesimally small point.
Expand Down Expand Up @@ -360,6 +361,9 @@ function _defineProps() {
_defineProperty('intensity', 1, function (newValue, oldValue) {
this.light.intensity = newValue;
});
_defineProperty('luminance', 0, function (newValue, oldValue) {
this.light.luminance = newValue;
});
_defineProperty('shape', LIGHTSHAPE_PUNCTUAL, function (newValue, oldValue) {
this.light.shape = newValue;
});
Expand Down
Loading

0 comments on commit 1da43a4

Please sign in to comment.