Reflector

Description

The Reflector object, included in the Threejs examples sub folders can be used to create mirrors.

The below example, extends the DragControls with GLTF Animations with several mirrors.

The reflector object will mirror in one direction only. If you want to create an infinity mirror, you can place another Reflector in front of the existing Reflector, but placed some distance in front of it and facing back towards at the first Reflector. The Reflectors will only reflect what they see in the current Render pass. So you won't get the realistic infinity mirror effect at first. Since Reflectors, only Reflect in one direction, you can place another Reflector behind an Existing Reflector in series, and you will get a triple reflection in the other Reflector reflecting back at both the others placed in series. In the example above, I have 4 Reflectors, all looking at each other. 2 are looking forward and 2 are looking back. And are all at different distances. Note that creating an infinity mirror using this technique will will require more CPU for each new Reflector added to the scene.

Code

./src/server/server.ts

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import express from "express"
import path from "path"
import http from "http"

const port: number = 3000

class App {
    private server: http.Server
    private port: number

    constructor(port: number) {
        this.port = port
        const app = express()
        app.use(express.static(path.join(__dirname, '../client')))
        app.use('/build/three.module.js', express.static(path.join(__dirname, '../../node_modules/three/build/three.module.js')))
        app.use('/jsm/controls/OrbitControls', express.static(path.join(__dirname, '../../node_modules/three/examples/jsm/controls/OrbitControls.js')))
        app.use('/jsm/loaders/GLTFLoader', express.static(path.join(__dirname, '../../node_modules/three/examples/jsm/loaders/GLTFLoader.js')))
        app.use('/jsm/libs/stats.module', express.static(path.join(__dirname, '../../node_modules/three/examples/jsm/libs/stats.module.js')))
        app.use('/jsm/controls/DragControls', express.static(path.join(__dirname, '../../node_modules/three/examples/jsm/controls/DragControls.js')))
        app.use('/jsm/objects/Reflector', express.static(path.join(__dirname, '../../node_modules/three/examples/jsm/objects/Reflector.js')))

        this.server = new http.Server(app);
    }

    public Start() {
        this.server.listen(this.port, () => {
            console.log( `Server listening on port ${this.port}.` )
        })
    }
}

new App(port).Start()

./src/client/client.ts

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
import * as THREE from '/build/three.module.js'
import { OrbitControls } from '/jsm/controls/OrbitControls'
import { GLTFLoader } from '/jsm/loaders/GLTFLoader'
import Stats from '/jsm/libs/stats.module'
import { DragControls } from '/jsm/controls/DragControls'
import { Reflector } from '/jsm/objects/Reflector'

const scene: THREE.Scene = new THREE.Scene()
const axesHelper = new THREE.AxesHelper(5)
scene.add(axesHelper)

var light1 = new THREE.PointLight();
light1.position.set(2.5, 2.5, 2.5)
light1.castShadow = true
scene.add(light1);

var light2 = new THREE.PointLight();
light2.position.set(-2.5, 2.5, 2.5)
light2.castShadow = true
scene.add(light2);


const camera: THREE.PerspectiveCamera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.01, 1000)
camera.position.set(0.8, 1.4, 1.0)

const renderer: THREE.WebGLRenderer = new THREE.WebGLRenderer()
renderer.setSize(window.innerWidth, window.innerHeight)
renderer.shadowMap.enabled = true
document.body.appendChild(renderer.domElement)

const orbitControls = new OrbitControls(camera, renderer.domElement)
orbitControls.screenSpacePanning = true
orbitControls.target.set(0, 1, 0)

const sceneMeshes: THREE.Mesh[] = new Array()
let boxHelper: THREE.BoxHelper

const dragControls = new DragControls(sceneMeshes, camera, renderer.domElement)
dragControls.addEventListener("hoveron", function (event) {
    boxHelper.visible = true
});
dragControls.addEventListener("hoveroff", function (event) {
    boxHelper.visible = false
});
dragControls.addEventListener("drag", function (event) {
    event.object.position.y = 0;
    orbitControls.enabled = false;
});
dragControls.addEventListener('dragstart', function (event) {
    boxHelper.visible = true
    orbitControls.enabled = false
})
dragControls.addEventListener('dragend', function (event) {
    boxHelper.visible = false
    orbitControls.enabled = true
})

const planeGeometry: THREE.PlaneGeometry = new THREE.PlaneGeometry(25, 25)
const texture = new THREE.TextureLoader().load("img/grid.png")
const plane: THREE.Mesh = new THREE.Mesh(planeGeometry, new THREE.MeshPhongMaterial({ map: texture }))
plane.rotateX(-Math.PI / 2)
plane.receiveShadow = true
scene.add(plane)

let mixer: THREE.AnimationMixer
let modelReady = false;
const gltfLoader: GLTFLoader = new GLTFLoader();
let modelGroup: THREE.Group
let modelDragBox: THREE.Mesh

gltfLoader.load(
    'models/eve@punching.glb',
    (gltf) => {
        gltf.scene.traverse(function (child) {
            if (child instanceof THREE.Group) {
                modelGroup = child
            }
            if ((<THREE.Mesh>child).isMesh) {
                child.castShadow = true
                child.frustumCulled = false;
                (child as THREE.Mesh).geometry.computeVertexNormals()
            }
        });

        mixer = new THREE.AnimationMixer(gltf.scene);
        mixer.clipAction((gltf as any).animations[0]).play()


        modelDragBox = new THREE.Mesh(new THREE.BoxGeometry(.5, 1.3, .5), new THREE.MeshBasicMaterial({ transparent: true, opacity: 0 }))
        modelDragBox.geometry.translate(0, .65, 0)
        scene.add(modelDragBox)
        sceneMeshes.push(modelDragBox);

        boxHelper = new THREE.BoxHelper(modelDragBox, 0xffff00)
        boxHelper.visible = false
        scene.add(boxHelper)

        scene.add(gltf.scene)

        modelReady = true
    },
    (xhr) => {
        console.log((xhr.loaded / xhr.total * 100) + '% loaded')
    },
    (error) => {
        console.log(error);
    }
)

window.addEventListener('resize', onWindowResize, false)
function onWindowResize() {
    camera.aspect = window.innerWidth / window.innerHeight
    camera.updateProjectionMatrix()
    renderer.setSize(window.innerWidth, window.innerHeight)
    render()
}


const mirrorBack1: Reflector = new Reflector(new THREE.PlaneBufferGeometry(2, 2), {
    color: new THREE.Color(0x7F7F7F),
    textureWidth: window.innerWidth * window.devicePixelRatio,
    textureHeight: window.innerHeight * window.devicePixelRatio,
});

mirrorBack1.position.y = 1
mirrorBack1.position.z = -1
scene.add(mirrorBack1);

const mirrorBack2: Reflector = new Reflector(new THREE.PlaneBufferGeometry(2, 2), {
    color: new THREE.Color(0x7F7F7F),
    textureWidth: window.innerWidth * window.devicePixelRatio,
    textureHeight: window.innerHeight * window.devicePixelRatio,
});

mirrorBack2.position.y = 1
mirrorBack2.position.z = -2
scene.add(mirrorBack2);

const mirrorFront1: Reflector = new Reflector(new THREE.PlaneBufferGeometry(2, 2), {
    color: new THREE.Color(0x7F7F7F),
    //clipBias: 0.003,
    textureWidth: window.innerWidth * window.devicePixelRatio,
    textureHeight: window.innerHeight * window.devicePixelRatio,
});
mirrorFront1.position.y = 1
mirrorFront1.position.z = 1
mirrorFront1.rotateY(Math.PI);
scene.add(mirrorFront1);


const mirrorFront2: Reflector = new Reflector(new THREE.PlaneBufferGeometry(2, 2), {
    color: new THREE.Color(0x7F7F7F),
    //clipBias: 0.003,
    textureWidth: window.innerWidth * window.devicePixelRatio,
    textureHeight: window.innerHeight * window.devicePixelRatio,
});
mirrorFront2.position.y = 1
mirrorFront2.position.z = 2
mirrorFront2.rotateY(Math.PI);
scene.add(mirrorFront2);

const stats = Stats()
document.body.appendChild(stats.dom)

const clock: THREE.Clock = new THREE.Clock()

var animate = function () {
    requestAnimationFrame(animate)

    orbitControls.update()

    if (modelReady) {
        mixer.update(clock.getDelta());
        modelGroup.position.copy(modelDragBox.position)
        boxHelper.update()
    }

    render()

    stats.update()
};

function render() {
    renderer.render(scene, camera)
}
animate();