|
| 1 | +import { |
| 2 | + DoubleSide, |
| 3 | + CanvasTexture, |
| 4 | + Mesh, |
| 5 | + MeshBasicMaterial, |
| 6 | + NodeMaterial, |
| 7 | + OrthographicCamera, |
| 8 | + PlaneGeometry, |
| 9 | + Scene, |
| 10 | + Texture |
| 11 | +} from 'three'; |
| 12 | +import { texture } from 'three/tsl'; |
| 13 | + |
| 14 | +/** |
| 15 | + * This is a helper for visualising a given light's shadow map. |
| 16 | + * It works for shadow casting lights: DirectionalLight and SpotLight. |
| 17 | + * It renders out the shadow map and displays it on a HUD. |
| 18 | + * |
| 19 | + * Example usage: |
| 20 | + * 1) Import ShadowMapViewer into your app. |
| 21 | + * |
| 22 | + * 2) Create a shadow casting light and name it optionally: |
| 23 | + * let light = new DirectionalLight( 0xffffff, 1 ); |
| 24 | + * light.castShadow = true; |
| 25 | + * light.name = 'Sun'; |
| 26 | + * |
| 27 | + * 3) Create a shadow map viewer for that light and set its size and position optionally: |
| 28 | + * let shadowMapViewer = new ShadowMapViewer( light ); |
| 29 | + * shadowMapViewer.size.set( 128, 128 ); //width, height default: 256, 256 |
| 30 | + * shadowMapViewer.position.set( 10, 10 ); //x, y in pixel default: 0, 0 (top left corner) |
| 31 | + * |
| 32 | + * 4) Render the shadow map viewer in your render loop: |
| 33 | + * shadowMapViewer.render( renderer ); |
| 34 | + * |
| 35 | + * 5) Optionally: Update the shadow map viewer on window resize: |
| 36 | + * shadowMapViewer.updateForWindowResize(); |
| 37 | + * |
| 38 | + * 6) If you set the position or size members directly, you need to call shadowMapViewer.update(); |
| 39 | + */ |
| 40 | + |
| 41 | +class ShadowMapViewer { |
| 42 | + |
| 43 | + constructor( light ) { |
| 44 | + |
| 45 | + //- Internals |
| 46 | + const scope = this; |
| 47 | + const doRenderLabel = ( light.name !== undefined && light.name !== '' ); |
| 48 | + let currentAutoClear; |
| 49 | + |
| 50 | + //Holds the initial position and dimension of the HUD |
| 51 | + const frame = { |
| 52 | + x: 10, |
| 53 | + y: 10, |
| 54 | + width: 256, |
| 55 | + height: 256 |
| 56 | + }; |
| 57 | + |
| 58 | + const camera = new OrthographicCamera( window.innerWidth / - 2, window.innerWidth / 2, window.innerHeight / 2, window.innerHeight / - 2, 1, 10 ); |
| 59 | + camera.position.set( 0, 0, 2 ); |
| 60 | + const scene = new Scene(); |
| 61 | + |
| 62 | + //HUD for shadow map |
| 63 | + |
| 64 | + const material = new NodeMaterial(); |
| 65 | + |
| 66 | + const shadowMapUniform = texture( new Texture() ); |
| 67 | + material.fragmentNode = shadowMapUniform; |
| 68 | + |
| 69 | + const plane = new PlaneGeometry( frame.width, frame.height ); |
| 70 | + const mesh = new Mesh( plane, material ); |
| 71 | + |
| 72 | + scene.add( mesh ); |
| 73 | + |
| 74 | + //Label for light's name |
| 75 | + let labelCanvas, labelMesh; |
| 76 | + |
| 77 | + if ( doRenderLabel ) { |
| 78 | + |
| 79 | + labelCanvas = document.createElement( 'canvas' ); |
| 80 | + |
| 81 | + const context = labelCanvas.getContext( '2d' ); |
| 82 | + context.font = 'Bold 20px Arial'; |
| 83 | + |
| 84 | + const labelWidth = context.measureText( light.name ).width; |
| 85 | + labelCanvas.width = labelWidth; |
| 86 | + labelCanvas.height = 25; //25 to account for g, p, etc. |
| 87 | + |
| 88 | + context.font = 'Bold 20px Arial'; |
| 89 | + context.fillStyle = 'rgba( 255, 0, 0, 1 )'; |
| 90 | + context.fillText( light.name, 0, 20 ); |
| 91 | + |
| 92 | + const labelTexture = new CanvasTexture( labelCanvas ); |
| 93 | + |
| 94 | + const labelMaterial = new MeshBasicMaterial( { map: labelTexture, side: DoubleSide, transparent: true } ); |
| 95 | + |
| 96 | + const labelPlane = new PlaneGeometry( labelCanvas.width, labelCanvas.height ); |
| 97 | + labelMesh = new Mesh( labelPlane, labelMaterial ); |
| 98 | + |
| 99 | + scene.add( labelMesh ); |
| 100 | + |
| 101 | + } |
| 102 | + |
| 103 | + function resetPosition() { |
| 104 | + |
| 105 | + scope.position.set( scope.position.x, scope.position.y ); |
| 106 | + |
| 107 | + } |
| 108 | + |
| 109 | + //- API |
| 110 | + // Set to false to disable displaying this shadow map |
| 111 | + this.enabled = true; |
| 112 | + |
| 113 | + // Set the size of the displayed shadow map on the HUD |
| 114 | + this.size = { |
| 115 | + width: frame.width, |
| 116 | + height: frame.height, |
| 117 | + set: function ( width, height ) { |
| 118 | + |
| 119 | + this.width = width; |
| 120 | + this.height = height; |
| 121 | + |
| 122 | + mesh.scale.set( this.width / frame.width, this.height / frame.height, 1 ); |
| 123 | + |
| 124 | + //Reset the position as it is off when we scale stuff |
| 125 | + resetPosition(); |
| 126 | + |
| 127 | + } |
| 128 | + }; |
| 129 | + |
| 130 | + // Set the position of the displayed shadow map on the HUD |
| 131 | + this.position = { |
| 132 | + x: frame.x, |
| 133 | + y: frame.y, |
| 134 | + set: function ( x, y ) { |
| 135 | + |
| 136 | + this.x = x; |
| 137 | + this.y = y; |
| 138 | + |
| 139 | + const width = scope.size.width; |
| 140 | + const height = scope.size.height; |
| 141 | + |
| 142 | + mesh.position.set( - window.innerWidth / 2 + width / 2 + this.x, window.innerHeight / 2 - height / 2 - this.y, 0 ); |
| 143 | + |
| 144 | + if ( doRenderLabel ) labelMesh.position.set( mesh.position.x, mesh.position.y - scope.size.height / 2 + labelCanvas.height / 2, 0 ); |
| 145 | + |
| 146 | + } |
| 147 | + }; |
| 148 | + |
| 149 | + this.render = function ( renderer ) { |
| 150 | + |
| 151 | + if ( this.enabled ) { |
| 152 | + |
| 153 | + //Because a light's .shadowMap is only initialised after the first render pass |
| 154 | + //we have to make sure the correct map is sent into the shader, otherwise we |
| 155 | + //always end up with the scene's first added shadow casting light's shadowMap |
| 156 | + //in the shader |
| 157 | + //See: https://github.com/mrdoob/three.js/issues/5932 |
| 158 | + shadowMapUniform.value = light.shadow.map.texture; |
| 159 | + |
| 160 | + currentAutoClear = renderer.autoClear; |
| 161 | + renderer.autoClear = false; // To allow render overlay |
| 162 | + renderer.clearDepth(); |
| 163 | + renderer.render( scene, camera ); |
| 164 | + renderer.autoClear = currentAutoClear; |
| 165 | + |
| 166 | + } |
| 167 | + |
| 168 | + }; |
| 169 | + |
| 170 | + this.updateForWindowResize = function () { |
| 171 | + |
| 172 | + if ( this.enabled ) { |
| 173 | + |
| 174 | + camera.left = window.innerWidth / - 2; |
| 175 | + camera.right = window.innerWidth / 2; |
| 176 | + camera.top = window.innerHeight / 2; |
| 177 | + camera.bottom = window.innerHeight / - 2; |
| 178 | + camera.updateProjectionMatrix(); |
| 179 | + |
| 180 | + this.update(); |
| 181 | + |
| 182 | + } |
| 183 | + |
| 184 | + }; |
| 185 | + |
| 186 | + this.update = function () { |
| 187 | + |
| 188 | + this.position.set( this.position.x, this.position.y ); |
| 189 | + this.size.set( this.size.width, this.size.height ); |
| 190 | + |
| 191 | + }; |
| 192 | + |
| 193 | + //Force an update to set position/size |
| 194 | + this.update(); |
| 195 | + |
| 196 | + } |
| 197 | + |
| 198 | +} |
| 199 | + |
| 200 | + |
| 201 | +export { ShadowMapViewer }; |
0 commit comments