Skip to content

Commit 2fa8e45

Browse files
Allow directional lights to automatically follow shadows (#5025)
* rename property * fix target updating * s/auto/automatic * address feedback * s/p1/pointToTest * s/out/resultPoint
1 parent 17ea7b1 commit 2fa8e45

File tree

6 files changed

+159
-17
lines changed

6 files changed

+159
-17
lines changed

docs/components/light.md

Lines changed: 22 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,13 @@ creating a child entity it targets. For example, pointing down its -Z axis:
9191
</a-light>
9292
```
9393

94-
Directional lights are the most efficient type for adding realtime shadows to a scene.
94+
Directional lights are the most efficient type for adding realtime shadows to a scene. You can use shadows like so:
95+
96+
```html
97+
<a-light type="directional" light="castShadow:true;" position="1 1 1" intensity="0.5" shdadow-camera-automatic="#objects"></a-light>
98+
```
99+
100+
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.
95101

96102
### Hemisphere
97103

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

188-
| Property | Light type | Description | Default Value |
189-
|---------------------|-----------------|--------------------------------------------------------------------------------------------------------------------------------------------|---------------|
190-
| castShadow | | Whether this light casts shadows on the scene. | false |
191-
| 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 |
192-
| shadowCameraBottom | `directional` | Bottom plane of shadow camera frustum. | -5 |
193-
| shadowCameraFar | | Far plane of shadow camera frustum. | 500 |
194-
| shadowCameraFov | `point`, `spot` | Shadow camera's FOV. | 50 |
195-
| shadowCameraLeft | `directional` | Left plane of shadow camera frustum. | -5 |
196-
| shadowCameraNear | | Near plane of shadow camera frustum. | 0.5 |
197-
| shadowCameraRight | `directional` | Right plane of shadow camera frustum. | 5 |
198-
| shadowCameraTop | `directional` | Top plane of shadow camera frustum. | 5 |
199-
| 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 |
200-
| shadowMapHeight | | Shadow map's vertical resolution. Larger shadow maps display more crisp shadows, at the cost of performance. | 512 |
201-
| shadowMapWidth | | Shadow map's horizontal resolution. | 512 |
194+
| Property | Light type | Description | Default Value |
195+
|-----------------------|-----------------|--------------------------------------------------------------------------------------------------------------------------------------------|---------------|
196+
| castShadow | | Whether this light casts shadows on the scene. | false |
197+
| 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 |
198+
| shadowCameraAutomatic | `directional` | Automatically configure the Bottom, Top, Left, Right and Near of a directional light's shadow map, from an element | |
199+
| shadowCameraBottom | `directional` | Bottom plane of shadow camera frustum. | -5 |
200+
| shadowCameraFar | | Far plane of shadow camera frustum. | 500 |
201+
| shadowCameraFov | `point`, `spot` | Shadow camera's FOV. | 50 |
202+
| shadowCameraLeft | `directional` | Left plane of shadow camera frustum. | -5 |
203+
| shadowCameraNear | | Near plane of shadow camera frustum. | 0.5 |
204+
| shadowCameraRight | `directional` | Right plane of shadow camera frustum. | 5 |
205+
| shadowCameraTop | `directional` | Top plane of shadow camera frustum. | 5 |
206+
| 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 |
207+
| shadowMapHeight | | Shadow map's vertical resolution. Larger shadow maps display more crisp shadows, at the cost of performance. | 512 |
208+
| shadowMapWidth | | Shadow map's horizontal resolution. | 512 |
202209

203210
### Adding Real-Time Shadows
204211

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
<!DOCTYPE html>
2+
<html>
3+
<head>
4+
<meta charset="utf-8">
5+
<title>Hello, AR World! • A-Frame</title>
6+
<meta name="description" content="Hello, AR World! • A-Frame">
7+
<script src="../../../dist/aframe-master.js"></script>
8+
<script>
9+
AFRAME.registerComponent('follow-shadow', {
10+
schema: {type: 'selector'},
11+
init() {this.el.object3D.renderOrder = -1;},
12+
tick() {
13+
if (this.data) {
14+
this.el.object3D.position.copy(this.data.object3D.position);
15+
this.el.object3D.position.y-=0.001; // stop z-fighting
16+
}
17+
}
18+
});
19+
</script>
20+
</head>
21+
<body>
22+
<a-scene
23+
reflection="directionalLight:a-light[type=directional]"
24+
ar-hit-test="target:#objects;"
25+
renderer="physicallyCorrectLights:true;colorManagement:true;exposure:1;toneMapping:ACESFilmic;"
26+
shadow="type:pcfsoft"
27+
>
28+
<a-light type="directional" light="castShadow:true;" position="1 1 1" intensity="0.5" shadow-camera-automatic="#objects"></a-light>
29+
<a-camera position="0 0.4 0" wasd-controls="acceleration:10;"></a-camera>
30+
<a-entity id="objects" scale="0.2 0.2 0.2" position="0 0 -1" shadow>
31+
<a-box position="-1 0.5 1" rotation="0 45 0" color="#4CC3D9"></a-box>
32+
<a-sphere position="0 1.25 -1" radius="1.25" color="#EF2D5E"></a-sphere>
33+
<a-cylinder position="1 0.75 1" radius="0.5" height="1.5" color="#FFC65D"></a-cylinder>
34+
</a-entity>
35+
<a-plane follow-shadow="#objects" material="shader:shadow" shadow="cast:false;" rotation="-90 0 0" width="2" height="2"></a-plane>
36+
<a-sky color="#ECECEC" hide-on-enter-ar></a-sky>
37+
</a-scene>
38+
</body>
39+
</html>

examples/index.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,7 @@ <h2>Examples</h2>
148148
<li><a href="showcase/spheres-and-fog/">Spheres and Fog</a></li>
149149
<li><a href="showcase/wikipedia/">Wikipedia</a></li>
150150
<li><a href="boilerplate/hello-world/">Hello World</a></li>
151+
<li><a href="boilerplate/ar-hello-world/">AR Hello World</a></li>
151152
<li><a href="boilerplate/panorama/">360&deg; Image</a></li>
152153
<li><a href="boilerplate/360-video/">360&deg; Video</a></li>
153154
<li><a href="boilerplate/3d-model/">3D Model (glTF)</a></li>

src/components/light.js

Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ var diff = utils.diff;
44
var debug = require('../utils/debug');
55
var registerComponent = require('../core/component').registerComponent;
66
var THREE = require('../lib/three');
7+
var mathUtils = require('../utils/math');
78

89
var degToRad = THREE.Math.degToRad;
910
var warn = debug('components:light:warn');
@@ -42,6 +43,7 @@ module.exports.Component = registerComponent('light', {
4243
shadowCameraBottom: {default: -5, if: {castShadow: true}},
4344
shadowCameraLeft: {default: -5, if: {castShadow: true}},
4445
shadowCameraVisible: {default: false, if: {castShadow: true}},
46+
shadowCameraAutomatic: {default: '', if: {type: ['directional']}},
4547
shadowMapHeight: {default: 512, if: {castShadow: true}},
4648
shadowMapWidth: {default: 512, if: {castShadow: true}},
4749
shadowRadius: {default: 1, if: {castShadow: true}}
@@ -111,7 +113,7 @@ module.exports.Component = registerComponent('light', {
111113
}
112114

113115
case 'envMap':
114-
this.updateProbeMap(data, light);
116+
self.updateProbeMap(data, light);
115117
break;
116118

117119
case 'castShadow':
@@ -133,6 +135,14 @@ module.exports.Component = registerComponent('light', {
133135
}
134136
break;
135137

138+
case 'shadowCameraAutomatic':
139+
if (data.shadowCameraAutomatic) {
140+
self.shadowCameraAutomaticEls = Array.from(document.querySelectorAll(data.shadowCameraAutomatic));
141+
} else {
142+
self.shadowCameraAutomaticEls = [];
143+
}
144+
break;
145+
136146
default: {
137147
light[key] = value;
138148
}
@@ -146,6 +156,50 @@ module.exports.Component = registerComponent('light', {
146156
this.updateShadow();
147157
},
148158

159+
tick: (function () {
160+
var bbox = new THREE.Box3();
161+
var normal = new THREE.Vector3();
162+
var cameraWorldPosition = new THREE.Vector3();
163+
var tempMat = new THREE.Matrix4();
164+
var sphere = new THREE.Sphere();
165+
var tempVector = new THREE.Vector3();
166+
167+
return function () {
168+
if (!(
169+
this.data.type === 'directional' &&
170+
this.light.shadow &&
171+
this.light.shadow.camera instanceof THREE.OrthographicCamera &&
172+
this.shadowCameraAutomaticEls.length
173+
)) return;
174+
175+
var camera = this.light.shadow.camera;
176+
camera.getWorldDirection(normal);
177+
camera.getWorldPosition(cameraWorldPosition);
178+
tempMat.copy(camera.matrixWorld);
179+
tempMat.invert();
180+
181+
camera.near = 1;
182+
camera.left = 100000;
183+
camera.right = -100000;
184+
camera.top = -100000;
185+
camera.bottom = 100000;
186+
this.shadowCameraAutomaticEls.forEach(function (el) {
187+
bbox.setFromObject(el.object3D);
188+
bbox.getBoundingSphere(sphere);
189+
var distanceToPlane = mathUtils.distanceOfPointFromPlane(cameraWorldPosition, normal, sphere.center);
190+
var pointOnCameraPlane = mathUtils.nearestPointInPlane(cameraWorldPosition, normal, sphere.center, tempVector);
191+
192+
var pointInXYPlane = pointOnCameraPlane.applyMatrix4(tempMat);
193+
camera.near = Math.min(-distanceToPlane - sphere.radius - 1, camera.near);
194+
camera.left = Math.min(-sphere.radius + pointInXYPlane.x, camera.left);
195+
camera.right = Math.max(sphere.radius + pointInXYPlane.x, camera.right);
196+
camera.top = Math.max(sphere.radius + pointInXYPlane.y, camera.top);
197+
camera.bottom = Math.min(-sphere.radius + pointInXYPlane.y, camera.bottom);
198+
});
199+
camera.updateProjectionMatrix();
200+
};
201+
}()),
202+
149203
setLight: function (data) {
150204
var el = this.el;
151205
var newLight = this.getLight(data);
@@ -168,6 +222,12 @@ module.exports.Component = registerComponent('light', {
168222
el.setObject3D('light-target', this.defaultTarget);
169223
el.getObject3D('light-target').position.set(0, 0, -1);
170224
}
225+
226+
if (data.shadowCameraAutomatic) {
227+
this.shadowCameraAutomaticEls = Array.from(document.querySelectorAll(data.shadowCameraAutomatic));
228+
} else {
229+
this.shadowCameraAutomaticEls = [];
230+
}
171231
}
172232
},
173233

src/extras/primitives/primitives/a-light.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ registerPrimitive('a-light', {
1515
penumbra: 'light.penumbra',
1616
type: 'light.type',
1717
target: 'light.target',
18-
envmap: 'light.envMap'
18+
envmap: 'light.envMap',
19+
'shadow-camera-automatic': 'light.shadowCameraAutomatic'
1920
}
2021
});

src/utils/math.js

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
/**
2+
* Find the disatance from a plane defined by a point on the plane and the normal of the plane to any point.
3+
* @param {THREE.Vector3} positionOnPlane any point on the plane.
4+
* @param {THREE.Vector3} planeNormal the normal of the plane
5+
* @param {THREE.Vector3} pointToTest point to test
6+
* @returns Number
7+
*/
8+
function distanceOfPointFromPlane (positionOnPlane, planeNormal, pointToTest) {
9+
// the d value in the plane equation a*x + b*y + c*z=d
10+
var d = planeNormal.dot(positionOnPlane);
11+
12+
// distance of point from plane
13+
return (d - planeNormal.dot(pointToTest)) / planeNormal.length();
14+
}
15+
16+
/**
17+
* Find the point on a plane that lies closest to
18+
* @param {THREE.Vector3} positionOnPlane any point on the plane.
19+
* @param {THREE.Vector3} planeNormal the normal of the plane
20+
* @param {THREE.Vector3} pointToTest point to test
21+
* @param {THREE.Vector3} resultPoint where to store the result.
22+
* @returns
23+
*/
24+
function nearestPointInPlane (positionOnPlane, planeNormal, pointToTest, resultPoint) {
25+
var t = distanceOfPointFromPlane(positionOnPlane, planeNormal, pointToTest);
26+
// closest point on the plane
27+
resultPoint.copy(planeNormal);
28+
resultPoint.multiplyScalar(t);
29+
resultPoint.add(pointToTest);
30+
return resultPoint;
31+
}
32+
33+
module.exports.distanceOfPointFromPlane = distanceOfPointFromPlane;
34+
module.exports.nearestPointInPlane = nearestPointInPlane;

0 commit comments

Comments
 (0)