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

Allow directional lights to automatically follow shadows #5025

Merged
merged 6 commits into from
Mar 25, 2022
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
37 changes: 22 additions & 15 deletions docs/components/light.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,13 @@ creating a child entity it targets. For example, pointing down its -Z axis:
</a-light>
```

Directional lights are the most efficient type for adding realtime shadows to a scene.
Directional lights are the most efficient type for adding realtime shadows to a scene. You can use shadows like so:

```html
<a-light type="directional" light="castShadow:true;" position="1 1 1" intensity="0.5" shdadow-camera-automatic="#objects"></a-light>
```

The `shdadow-camera-automatic` configuration maps to `light.shadowCameraAutomatic` which tells the light to automatically update the shadow camera to be the minimum size and position to encompass the target elements.

### Hemisphere

Expand Down Expand Up @@ -185,20 +191,21 @@ is very helpful to **[use the A-Frame Inspector to configure shadows][inspector]
Light types that support shadows (`point`, `spot`, and `directional`) include
additional properties:

| Property | Light type | Description | Default Value |
|---------------------|-----------------|--------------------------------------------------------------------------------------------------------------------------------------------|---------------|
| castShadow | | Whether this light casts shadows on the scene. | false |
| shadowBias | | Offset depth when deciding whether a surface is in shadow. Tiny adjustments here (in the order of +/-0.0001) may reduce artifacts in shadows. | 0 |
| shadowCameraBottom | `directional` | Bottom plane of shadow camera frustum. | -5 |
| shadowCameraFar | | Far plane of shadow camera frustum. | 500 |
| shadowCameraFov | `point`, `spot` | Shadow camera's FOV. | 50 |
| shadowCameraLeft | `directional` | Left plane of shadow camera frustum. | -5 |
| shadowCameraNear | | Near plane of shadow camera frustum. | 0.5 |
| shadowCameraRight | `directional` | Right plane of shadow camera frustum. | 5 |
| shadowCameraTop | `directional` | Top plane of shadow camera frustum. | 5 |
| shadowCameraVisible | | Displays a visual aid showing the shadow camera's position and frustum. This is the light's view of the scene, used to project shadows. | false |
| shadowMapHeight | | Shadow map's vertical resolution. Larger shadow maps display more crisp shadows, at the cost of performance. | 512 |
| shadowMapWidth | | Shadow map's horizontal resolution. | 512 |
| Property | Light type | Description | Default Value |
|-----------------------|-----------------|--------------------------------------------------------------------------------------------------------------------------------------------|---------------|
| castShadow | | Whether this light casts shadows on the scene. | false |
| shadowBias | | Offset depth when deciding whether a surface is in shadow. Tiny adjustments here (in the order of +/-0.0001) may reduce artifacts in shadows. | 0 |
| shadowCameraAutomatic | `directional` | Automatically configure the Bottom, Top, Left, Right and Near of a directional light's shadow map, from an element | |
| shadowCameraBottom | `directional` | Bottom plane of shadow camera frustum. | -5 |
| shadowCameraFar | | Far plane of shadow camera frustum. | 500 |
| shadowCameraFov | `point`, `spot` | Shadow camera's FOV. | 50 |
| shadowCameraLeft | `directional` | Left plane of shadow camera frustum. | -5 |
| shadowCameraNear | | Near plane of shadow camera frustum. | 0.5 |
| shadowCameraRight | `directional` | Right plane of shadow camera frustum. | 5 |
| shadowCameraTop | `directional` | Top plane of shadow camera frustum. | 5 |
| shadowCameraVisible | | Displays a visual aid showing the shadow camera's position and frustum. This is the light's view of the scene, used to project shadows. | false |
| shadowMapHeight | | Shadow map's vertical resolution. Larger shadow maps display more crisp shadows, at the cost of performance. | 512 |
| shadowMapWidth | | Shadow map's horizontal resolution. | 512 |

### Adding Real-Time Shadows

Expand Down
39 changes: 39 additions & 0 deletions examples/boilerplate/ar-hello-world/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Hello, World! • A-Frame</title>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hello, AR World! perhaps to differentiate

<meta name="description" content="Hello, World! • A-Frame">
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hello, AR World! perhaps

<script src="../../../dist/aframe-master.js"></script>
<script>
AFRAME.registerComponent('follow-shadow', {
schema: {type: 'selector'},
init() {this.el.object3D.renderOrder = -1;},
tick() {
if (this.data) {
this.el.object3D.position.copy(this.data.object3D.position);
this.el.object3D.position.y-=0.001; // stop z-fighting
}
}
});
</script>
</head>
<body>
<a-scene
reflection="directionalLight:a-light[type=directional]"
ar-hit-test="target:#objects;"
renderer="physicallyCorrectLights:true;colorManagement:true;exposure:1;toneMapping:ACESFilmic;"
shadow="type:pcfsoft"
>
<a-light type="directional" light="castShadow:true;" position="1 1 1" intensity="0.5" shadow-camera-automatic="#objects"></a-light>
<a-camera position="0 0.4 0" wasd-controls="acceleration:10;"></a-camera>
<a-entity id="objects" scale="0.2 0.2 0.2" position="0 0 -1" shadow>
<a-box position="-1 0.5 1" rotation="0 45 0" color="#4CC3D9"></a-box>
<a-sphere position="0 1.25 -1" radius="1.25" color="#EF2D5E"></a-sphere>
<a-cylinder position="1 0.75 1" radius="0.5" height="1.5" color="#FFC65D"></a-cylinder>
</a-entity>
<a-plane follow-shadow="#objects" material="shader:shadow" shadow="cast:false;" rotation="-90 0 0" width="2" height="2"></a-plane>
<a-sky color="#ECECEC" hide-on-enter-ar></a-sky>
</a-scene>
</body>
</html>
1 change: 1 addition & 0 deletions examples/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,7 @@ <h2>Examples</h2>
<li><a href="showcase/spheres-and-fog/">Spheres and Fog</a></li>
<li><a href="showcase/wikipedia/">Wikipedia</a></li>
<li><a href="boilerplate/hello-world/">Hello World</a></li>
<li><a href="boilerplate/ar-hello-world/">AR Hello World</a></li>
<li><a href="boilerplate/panorama/">360&deg; Image</a></li>
<li><a href="boilerplate/360-video/">360&deg; Video</a></li>
<li><a href="boilerplate/3d-model/">3D Model (glTF)</a></li>
Expand Down
78 changes: 77 additions & 1 deletion src/components/light.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,23 @@ var CubeLoader = new THREE.CubeTextureLoader();

var probeCache = {};

function distanceOfPointFromPlane (positionOnPlane, planeNormal, p1) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe factor out into utils/math.js to keep this tidier.

// the d value in the plane equation a*x + b*y + c*z=d
var d = planeNormal.dot(positionOnPlane);

// distance of point from plane
return (d - planeNormal.dot(p1)) / planeNormal.length();
}

function nearestPointInPlane (positionOnPlane, planeNormal, p1, out) {
var t = distanceOfPointFromPlane(positionOnPlane, planeNormal, p1);
// closest point on the plane
out.copy(planeNormal);
out.multiplyScalar(t);
out.add(p1);
return out;
}

/**
* Light component.
*/
Expand Down Expand Up @@ -42,6 +59,7 @@ module.exports.Component = registerComponent('light', {
shadowCameraBottom: {default: -5, if: {castShadow: true}},
shadowCameraLeft: {default: -5, if: {castShadow: true}},
shadowCameraVisible: {default: false, if: {castShadow: true}},
shadowCameraAutomatic: {default: '', if: {type: ['directional']}},
shadowMapHeight: {default: 512, if: {castShadow: true}},
shadowMapWidth: {default: 512, if: {castShadow: true}},
shadowRadius: {default: 1, if: {castShadow: true}}
Expand Down Expand Up @@ -111,7 +129,7 @@ module.exports.Component = registerComponent('light', {
}

case 'envMap':
this.updateProbeMap(data, light);
self.updateProbeMap(data, light);
break;

case 'castShadow':
Expand All @@ -133,6 +151,14 @@ module.exports.Component = registerComponent('light', {
}
break;

case 'shadowCameraAutomatic':
if (data.shadowCameraAutomatic) {
self.shadowCameraAutomaticEls = Array.from(document.querySelectorAll(data.shadowCameraAutomatic));
} else {
self.shadowCameraAutomaticEls = [];
}
break;

default: {
light[key] = value;
}
Expand All @@ -146,6 +172,50 @@ module.exports.Component = registerComponent('light', {
this.updateShadow();
},

tick: (function tickSetup () {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no need to name this function

var bbox = new THREE.Box3();
var normal = new THREE.Vector3();
var cameraWorldPosition = new THREE.Vector3();
var tempMat = new THREE.Matrix4();
var sphere = new THREE.Sphere();
var tempVector = new THREE.Vector3();

return function tick () {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no need to name this function

if (
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

anyway to simplify this condition and do an early return for readability?

if (XXX) { return; }

this.data.type === 'directional' &&
this.light.shadow &&
this.light.shadow.camera instanceof THREE.OrthographicCamera &&
this.shadowCameraAutomaticEls.length
) {
var camera = this.light.shadow.camera;
camera.getWorldDirection(normal);
camera.getWorldPosition(cameraWorldPosition);
tempMat.copy(camera.matrixWorld);
tempMat.invert();

camera.near = 1;
camera.left = 100000;
camera.right = -100000;
camera.top = -100000;
camera.bottom = 100000;
this.shadowCameraAutomaticEls.forEach(function (el) {
bbox.setFromObject(el.object3D);
bbox.getBoundingSphere(sphere);
var distanceToPlane = distanceOfPointFromPlane(cameraWorldPosition, normal, sphere.center);
var pointOnCameraPlane = nearestPointInPlane(cameraWorldPosition, normal, sphere.center, tempVector);

var pointInXYPlane = pointOnCameraPlane.applyMatrix4(tempMat);
camera.near = Math.min(-distanceToPlane - sphere.radius - 1, camera.near);
camera.left = Math.min(-sphere.radius + pointInXYPlane.x, camera.left);
camera.right = Math.max(sphere.radius + pointInXYPlane.x, camera.right);
camera.top = Math.max(sphere.radius + pointInXYPlane.y, camera.top);
camera.bottom = Math.min(-sphere.radius + pointInXYPlane.y, camera.bottom);
});
camera.updateProjectionMatrix();
}
};
}()),

setLight: function (data) {
var el = this.el;
var newLight = this.getLight(data);
Expand All @@ -168,6 +238,12 @@ module.exports.Component = registerComponent('light', {
el.setObject3D('light-target', this.defaultTarget);
el.getObject3D('light-target').position.set(0, 0, -1);
}

if (data.shadowCameraAutomatic) {
this.shadowCameraAutomaticEls = Array.from(document.querySelectorAll(data.shadowCameraAutomatic));
} else {
this.shadowCameraAutomaticEls = [];
}
}
},

Expand Down
3 changes: 2 additions & 1 deletion src/extras/primitives/primitives/a-light.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ registerPrimitive('a-light', {
penumbra: 'light.penumbra',
type: 'light.type',
target: 'light.target',
envmap: 'light.envMap'
envmap: 'light.envMap',
'shadow-camera-automatic': 'light.shadowCameraAutomatic'
}
});