├── .eslintignore ├── .gitignore ├── .eslintrc ├── .prettierignore ├── screenshot.png ├── examples ├── images │ ├── uv.jpg │ ├── example-basic.png │ ├── charles-unsplash.jpg │ ├── example-3d-model.png │ ├── example-instancing.png │ ├── example-same-camera.png │ └── source.svg ├── lib │ ├── three-utils.js │ ├── loadTexture.js │ ├── loadEnvMap.js │ ├── AssetManager.js │ └── WebGLApp.js ├── basic.html ├── 3d-model.html ├── instancing.html ├── same-camera.html ├── basic.js ├── 3d-model.js ├── same-camera.js ├── instancing.js └── index.html ├── .babelrc ├── .prettierrc ├── .editorconfig ├── src ├── three-utils.js └── ProjectedMaterial.js ├── rollup.config.js ├── package.json ├── webpack.config.js └── README.md /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | build/ -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | *.log 3 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "eslint-config-accurapp" 3 | } 4 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | package.json 2 | .* 3 | node_modules/ 4 | build/ -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/juanuys/three-projected-material/master/screenshot.png -------------------------------------------------------------------------------- /examples/images/uv.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/juanuys/three-projected-material/master/examples/images/uv.jpg -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["accurapp"], 3 | "plugins": [ 4 | "@babel/plugin-transform-runtime" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 100, 3 | "semi": false, 4 | "singleQuote": true, 5 | "trailingComma": "es5" 6 | } 7 | -------------------------------------------------------------------------------- /examples/images/example-basic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/juanuys/three-projected-material/master/examples/images/example-basic.png -------------------------------------------------------------------------------- /examples/images/charles-unsplash.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/juanuys/three-projected-material/master/examples/images/charles-unsplash.jpg -------------------------------------------------------------------------------- /examples/images/example-3d-model.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/juanuys/three-projected-material/master/examples/images/example-3d-model.png -------------------------------------------------------------------------------- /examples/images/example-instancing.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/juanuys/three-projected-material/master/examples/images/example-instancing.png -------------------------------------------------------------------------------- /examples/images/example-same-camera.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/juanuys/three-projected-material/master/examples/images/example-same-camera.png -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /examples/images/source.svg: -------------------------------------------------------------------------------- 1 | 5 | -------------------------------------------------------------------------------- /src/three-utils.js: -------------------------------------------------------------------------------- 1 | export function monkeyPatch(shader, { header = '', main = '', ...replaces }) { 2 | let patchedShader = shader 3 | 4 | Object.keys(replaces).forEach(key => { 5 | patchedShader = patchedShader.replace(key, replaces[key]) 6 | }) 7 | 8 | return patchedShader.replace( 9 | 'void main() {', 10 | ` 11 | ${header} 12 | void main() { 13 | ${main} 14 | ` 15 | ) 16 | } -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | input: 'src/ProjectedMaterial.js', 3 | external: ['three'], 4 | output: [ 5 | { 6 | format: 'umd', 7 | globals: { 8 | three: 'THREE', 9 | }, 10 | name: 'projectedMaterial', 11 | exports: 'named', 12 | file: 'build/ProjectedMaterial.js', 13 | }, 14 | { 15 | format: 'esm', 16 | file: 'build/ProjectedMaterial.module.js', 17 | }, 18 | ], 19 | } 20 | -------------------------------------------------------------------------------- /examples/lib/three-utils.js: -------------------------------------------------------------------------------- 1 | // from https://discourse.threejs.org/t/functions-to-calculate-the-visible-width-height-at-a-given-z-depth-from-a-perspective-camera/269 2 | export function visibleHeightAtZDepth(depth, camera) { 3 | // compensate for cameras not positioned at z=0 4 | const cameraOffset = camera.position.z 5 | if (depth < cameraOffset) { 6 | depth -= cameraOffset 7 | } else { 8 | depth += cameraOffset 9 | } 10 | 11 | // vertical fov in radians 12 | const vFOV = (camera.fov * Math.PI) / 180 13 | 14 | // Math.abs to ensure the result is always positive 15 | return 2 * Math.tan(vFOV / 2) * Math.abs(depth) 16 | } 17 | 18 | export function visibleWidthAtZDepth(depth, camera) { 19 | const height = visibleHeightAtZDepth(depth, camera) 20 | return height * camera.aspect 21 | } 22 | -------------------------------------------------------------------------------- /examples/basic.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 | 6 |132 | Three.js Material which lets you do 133 | Texture Projection 136 | on a 3d Model. 137 |
138 |
159 |
170 |
](https://marcofugaro.github.io/three-projected-material/)
6 |
7 | ### [EXAMPLES](https://marcofugaro.github.io/three-projected-material/)
8 |
9 | ## Installation
10 |
11 | After having installed Three.js, install it from npm with:
12 |
13 | ```
14 | npm install three-projected-material
15 | ```
16 |
17 | or
18 |
19 | ```
20 | yarn add three-projected-material
21 | ```
22 |
23 | You can also use it from the CDN, just make sure to put this after the Three.js script:
24 |
25 | ```html
26 |
27 | ```
28 |
29 | ## Getting started
30 |
31 | You can import it like this
32 |
33 | ```js
34 | import ProjectedMaterial, { project } from 'three-projected-material'
35 | ```
36 |
37 | or, if you're using CommonJS
38 |
39 | ```js
40 | const { default: ProjectedMaterial, project } = require('three-projected-material')
41 | ```
42 |
43 | Instead, if you install it from the CDN, its exposed under `window.projectedMaterial`, and you use it like this
44 |
45 | ```js
46 | const { default: ProjectedMaterial, project } = window.projectedMaterial
47 | ```
48 |
49 | Then, you can use it like this:
50 |
51 | ```js
52 | const geometry = new THREE.BoxGeometry(1, 1, 1)
53 | const material = new ProjectedMaterial({
54 | camera, // the camera that acts as a projector
55 | texture, // the texture being projected
56 | color: '#cccccc', // the color of the object if it's not projected on
57 | textureScale: 0.8, // scale down the texture a bit
58 | cover: true, // enable background-size: cover behaviour, by default it's like background-size: contain
59 | })
60 | const box = new THREE.Mesh(geometry, material)
61 | webgl.scene.add(box)
62 |
63 | // move the mesh any way you want!
64 | box.rotation.y = -Math.PI / 4
65 |
66 | // and when you're ready project the texture!
67 | project(box)
68 | ```
69 |
70 | ProjectedMaterial also supports instanced objects via Three.js' [InstancedMesh](https://threejs.org/docs/index.html#api/en/objects/InstancedMesh), this is an example usage:
71 |
72 | ```js
73 | import ProjectedMaterial, {
74 | allocateProjectionData,
75 | projectInstanceAt,
76 | } from 'three-projected-material'
77 |
78 | const NUM_ELEMENTS = 1000
79 | const dummy = new THREE.Object3D()
80 |
81 | const geometry = new THREE.BoxBufferGeometry(1, 1, 1)
82 | const material = new ProjectedMaterial({
83 | camera,
84 | texture,
85 | color: '#cccccc',
86 | instanced: true,
87 | })
88 |
89 | // allocate the projection data
90 | allocateProjectionData(geometry, NUM_ELEMENTS)
91 |
92 | // create the instanced mesh
93 | const instancedMesh = new THREE.InstancedMesh(geometry, material, NUM_ELEMENTS)
94 |
95 | for (let i = 0; i < NUM_ELEMENTS; i++) {
96 | // position the element
97 | dummy.position.x = random(-width / 2, width / 2)
98 | dummy.position.y = random(-height / 2, height / 2)
99 | dummy.rotation.x = random(0, Math.PI * 2)
100 | dummy.rotation.y = random(0, Math.PI * 2)
101 | dummy.rotation.z = random(0, Math.PI * 2)
102 | dummy.updateMatrix()
103 | instancedMesh.setMatrixAt(i, dummy.matrix)
104 |
105 | // project the texture!
106 | dummy.updateMatrixWorld()
107 | projectInstanceAt(i, instancedMesh, dummy.matrixWorld)
108 | }
109 |
110 | webgl.scene.add(instancedMesh)
111 | ```
112 |
113 | If you want to see the remaining code, and other usages, check out the [examples](https://marcofugaro.github.io/three-projected-material/).
114 |
115 | ## API
116 |
117 | ### new ProjectedMaterial({ camera, texture, ...others })
118 |
119 | Create a new material to later use for a mesh.
120 |
121 | | Option | Default | Description |
122 | | -------------- | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
123 | | `camera` | | The [PerspectiveCamera](https://threejs.org/docs/#api/en/cameras/PerspectiveCamera) the texture will be projected from. |
124 | | `texture` | | The [Texture](https://threejs.org/docs/#api/en/textures/Texture) being projected. |
125 | | `color` | `'#ffffff'` | The color the non-projected on parts of the object will have. |
126 | | `textureScale` | 1 | Make the texture bigger or smaller. |
127 | | `cover` | false | Wheter the texture should act like [`background-size: cover`](https://css-tricks.com/almanac/properties/b/background-size/) on the projector frustum. By default it works like [`background-size: contain`](https://css-tricks.com/almanac/properties/b/background-size/). |
128 | | `instanced` | false | Wether the material will be part of an [InstancedMesh](https://threejs.org/docs/#api/en/objects/InstancedMesh). If this is true, [`allocateProjectionData()`](#allocateprojectiondatageometry-instancescount) and [`projectInstanceAt()`](#projectinstanceatindex-instancedmesh-matrixworld) must be used instead of [`project()`](#projectmesh). |
129 | | `opacity` | 1 | The opacity of the material, works like the [`Material.opacity`](https://threejs.org/docs/#api/en/materials/Material.opacity). |
130 |
131 | ### project(mesh)
132 |
133 | Project the texture from the camera on the mesh.
134 |
135 | | Option | Description |
136 | | ------ | ---------------------------------------------------- |
137 | | `mesh` | The mesh that has a `ProjectedMaterial` as material. |
138 |
139 | ### allocateProjectionData(geometry, instancesCount)
140 |
141 | Allocate the data that will be used when projecting on an [InstancedMesh](https://threejs.org/docs/#api/en/objects/InstancedMesh). Use this on the geometry that will be used in pair with a `ProjectedMaterial` when initializing `InstancedMesh`.
142 |
143 | _**NOTE:** Don't forget to pass `instanced: true` to the projected material._
144 |
145 | | Option | Description |
146 | | ---------------- | ----------------------------------------------------------------------------- |
147 | | `geometry` | The geometry that will be passed to the `InstancedMesh`. |
148 | | `instancesCount` | The number of instances, the same that will be passed to the `InstancedMesh`. |
149 |
150 | ### projectInstanceAt(index, instancedMesh, matrixWorld)
151 |
152 | Do the projection for an [InstancedMesh](https://threejs.org/docs/#api/en/objects/InstancedMesh). Don't forget to call `updateMatrixWorld()` like you do before calling `InstancedMesh.setMatrixAt()`.
153 |
154 | ```js
155 | dummy.updateMatrixWorld()
156 | projectInstanceAt(i, instancedMesh, dummy.matrixWorld)
157 | ```
158 |
159 | [Link to the full example](https://marcofugaro.github.io/three-projected-material/instancing).
160 |
161 | _**NOTE:** Don't forget to pass `instanced: true` to the projected material._
162 |
163 | | Option | Description |
164 | | --------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
165 | | `index` | The index of the instanced element to project. |
166 | | `instancedMesh` | The [InstancedMesh](https://threejs.org/docs/#api/en/objects/InstancedMesh) with a projected material. |
167 | | `matrixWorld` | The `matrixWorld` of the dummy you used to position the instanced mesh element. Be sure to call `.updateMatrixWorld()` beforehand. |
168 |
169 | ## TODO
170 |
171 | - different materials for the rest of the object
172 | - multiple projections onto an object?
173 |
--------------------------------------------------------------------------------
/examples/lib/WebGLApp.js:
--------------------------------------------------------------------------------
1 | // Taken from https://github.com/marcofugaro/threejs-modern-app/blob/master/src/lib/WebGLApp.js
2 | import * as THREE from 'three'
3 | import createOrbitControls from 'orbit-controls'
4 | import createTouches from 'touches'
5 | import dataURIToBlob from 'datauritoblob'
6 | import Stats from 'stats.js'
7 | import State from 'controls-state'
8 | import wrapGUI from 'controls-gui'
9 | import { getGPUTier } from 'detect-gpu'
10 | import { EffectComposer } from 'three/examples/jsm/postprocessing/EffectComposer'
11 | import { RenderPass } from 'three/examples/jsm/postprocessing/RenderPass'
12 |
13 | export default class WebGLApp {
14 | #updateListeners = []
15 | #tmpTarget = new THREE.Vector3()
16 | #rafID
17 | #lastTime
18 |
19 | constructor({
20 | background = '#000',
21 | backgroundAlpha = 1,
22 | fov = 45,
23 | near = 0.01,
24 | far = 100,
25 | ...options
26 | } = {}) {
27 | this.renderer = new THREE.WebGLRenderer({
28 | antialias: true,
29 | alpha: false,
30 | // enabled for saving screenshots of the canvas,
31 | // may wish to disable this for perf reasons
32 | preserveDrawingBuffer: true,
33 | failIfMajorPerformanceCaveat: true,
34 | ...options,
35 | })
36 |
37 | this.renderer.sortObjects = false
38 | this.canvas = this.renderer.domElement
39 |
40 | this.renderer.setClearColor(background, backgroundAlpha)
41 |
42 | // clamp pixel ratio for performance
43 | this.maxPixelRatio = options.maxPixelRatio || 2
44 | // clamp delta to stepping anything too far forward
45 | this.maxDeltaTime = options.maxDeltaTime || 1 / 30
46 |
47 | // setup a basic camera
48 | this.camera = new THREE.PerspectiveCamera(fov, 1, near, far)
49 | // this.camera = new THREE.OrthographicCamera(-2, 2, 2, -2, near, far)
50 |
51 | this.scene = new THREE.Scene()
52 |
53 | this.gl = this.renderer.getContext()
54 |
55 | this.time = 0
56 | this.isRunning = false
57 | this.#lastTime = performance.now()
58 | this.#rafID = null
59 |
60 | // detect the gpu info
61 | const gpu = getGPUTier({ glContext: this.renderer.getContext() })
62 | this.gpu = {
63 | name: gpu.type,
64 | tier: Number(gpu.tier.slice(-1)),
65 | isMobile: gpu.tier.toLowerCase().includes('mobile'),
66 | }
67 |
68 | // handle resize events
69 | window.addEventListener('resize', this.resize)
70 | window.addEventListener('orientationchange', this.resize)
71 |
72 | // force an initial resize event
73 | this.resize()
74 |
75 | // __________________________ADDONS__________________________
76 |
77 | // really basic touch handler that propagates through the scene
78 | this.touchHandler = createTouches(this.canvas, {
79 | target: this.canvas,
80 | filtered: true,
81 | })
82 | this.touchHandler.on('start', (ev, pos) => this.traverse('onPointerDown', ev, pos))
83 | this.touchHandler.on('move', (ev, pos) => this.traverse('onPointerMove', ev, pos))
84 | this.touchHandler.on('end', (ev, pos) => this.traverse('onPointerUp', ev, pos))
85 |
86 | // expose a composer for postprocessing passes
87 | if (options.postprocessing) {
88 | this.composer = new EffectComposer(this.renderer)
89 | this.composer.addPass(new RenderPass(this.scene, this.camera))
90 | }
91 |
92 | // set up a simple orbit controller
93 | if (options.orbitControls) {
94 | this.orbitControls = createOrbitControls({
95 | element: this.canvas,
96 | parent: window,
97 | distance: 4,
98 | ...(options.orbitControls instanceof Object ? options.orbitControls : {}),
99 | })
100 |
101 | // move the camera position accordingly to the orgitcontrols options
102 | this.camera.position.fromArray(this.orbitControls.position)
103 | this.camera.lookAt(new THREE.Vector3().fromArray(this.orbitControls.target))
104 | }
105 |
106 | // Attach the Cannon physics engine
107 | if (options.world) this.world = options.world
108 |
109 | // Attach Tween.js
110 | if (options.tween) this.tween = options.tween
111 |
112 | // show the fps meter
113 | if (options.showFps) {
114 | this.stats = new Stats()
115 | this.stats.showPanel(0)
116 | document.body.appendChild(this.stats.dom)
117 | }
118 |
119 | // initialize the controls-state
120 | if (options.controls) {
121 | const controlsState = State(options.controls)
122 | this.controls = options.hideControls ? controlsState : wrapGUI(controlsState)
123 | }
124 | }
125 |
126 | resize = ({
127 | width = window.innerWidth,
128 | height = window.innerHeight,
129 | pixelRatio = Math.min(this.maxPixelRatio, window.devicePixelRatio),
130 | } = {}) => {
131 | this.width = width
132 | this.height = height
133 | this.pixelRatio = pixelRatio
134 |
135 | // update pixel ratio if necessary
136 | if (this.renderer.getPixelRatio() !== pixelRatio) {
137 | this.renderer.setPixelRatio(pixelRatio)
138 | }
139 |
140 | // setup new size & update camera aspect if necessary
141 | this.renderer.setSize(width, height)
142 | if (this.camera.isPerspectiveCamera) {
143 | this.camera.aspect = width / height
144 | }
145 | this.camera.updateProjectionMatrix()
146 |
147 | // resize also the composer
148 | if (this.composer) {
149 | this.composer.setSize(pixelRatio * width, pixelRatio * height)
150 | }
151 |
152 | // recursively tell all child objects to resize
153 | this.scene.traverse(obj => {
154 | if (typeof obj.resize === 'function') {
155 | obj.resize({
156 | width,
157 | height,
158 | pixelRatio,
159 | })
160 | }
161 | })
162 |
163 | // draw a frame to ensure the new size has been registered visually
164 | this.draw()
165 | return this
166 | }
167 |
168 | // convenience function to trigger a PNG download of the canvas
169 | saveScreenshot = ({ width = 2560, height = 1440, fileName = 'image.png' } = {}) => {
170 | // force a specific output size
171 | this.resize({ width, height, pixelRatio: 1 })
172 | this.draw()
173 |
174 | const dataURI = this.canvas.toDataURL('image/png')
175 |
176 | // reset to default size
177 | this.resize()
178 | this.draw()
179 |
180 | // save
181 | saveDataURI(fileName, dataURI)
182 | }
183 |
184 | update = (dt, time) => {
185 | if (this.orbitControls) {
186 | this.orbitControls.update()
187 |
188 | // reposition to orbit controls
189 | this.camera.up.fromArray(this.orbitControls.up)
190 | this.camera.position.fromArray(this.orbitControls.position)
191 | this.#tmpTarget.fromArray(this.orbitControls.target)
192 | this.camera.lookAt(this.#tmpTarget)
193 | }
194 |
195 | // recursively tell all child objects to update
196 | this.scene.traverse(obj => {
197 | if (typeof obj.update === 'function') {
198 | obj.update(dt, time)
199 | }
200 | })
201 |
202 | if (this.world) {
203 | // update the Cannon physics engine
204 | this.world.step(dt)
205 |
206 | // recursively tell all child bodies to update
207 | this.world.bodies.forEach(body => {
208 | if (typeof body.update === 'function') {
209 | body.update(dt, time)
210 | }
211 | })
212 | }
213 |
214 | if (this.tween) {
215 | // update the Tween.js engine
216 | this.tween.update()
217 | }
218 |
219 | // call the update listeners
220 | this.#updateListeners.forEach(fn => fn(dt, time))
221 |
222 | return this
223 | }
224 |
225 | onUpdate(fn) {
226 | this.#updateListeners.push(fn)
227 | }
228 |
229 | draw = () => {
230 | if (this.composer) {
231 | // make sure to always render the last pass
232 | this.composer.passes.forEach((pass, i, passes) => {
233 | const isLastElement = i === passes.length - 1
234 |
235 | if (isLastElement) {
236 | pass.renderToScreen = true
237 | } else {
238 | pass.renderToScreen = false
239 | }
240 | })
241 |
242 | this.composer.render()
243 | } else {
244 | this.renderer.render(this.scene, this.camera)
245 | }
246 | return this
247 | }
248 |
249 | start = () => {
250 | if (this.#rafID !== null) return
251 | this.#rafID = window.requestAnimationFrame(this.animate)
252 | this.isRunning = true
253 | return this
254 | }
255 |
256 | stop = () => {
257 | if (this.#rafID === null) return
258 | window.cancelAnimationFrame(this.#rafID)
259 | this.#rafID = null
260 | this.isRunning = false
261 | return this
262 | }
263 |
264 | animate = () => {
265 | if (!this.isRunning) return
266 | window.requestAnimationFrame(this.animate)
267 |
268 | if (this.stats) this.stats.begin()
269 |
270 | const now = performance.now()
271 | const dt = Math.min(this.maxDeltaTime, (now - this.#lastTime) / 1000)
272 | this.time += dt
273 | this.#lastTime = now
274 | this.update(dt, this.time)
275 | this.draw()
276 |
277 | if (this.stats) this.stats.end()
278 | }
279 |
280 | traverse = (fn, ...args) => {
281 | this.scene.traverse(child => {
282 | if (typeof child[fn] === 'function') {
283 | child[fn].apply(child, args)
284 | }
285 | })
286 | }
287 | }
288 |
289 | function saveDataURI(name, dataURI) {
290 | const blob = dataURIToBlob(dataURI)
291 |
292 | // force download
293 | const link = document.createElement('a')
294 | link.download = name
295 | link.href = window.URL.createObjectURL(blob)
296 | link.onclick = setTimeout(() => {
297 | window.URL.revokeObjectURL(blob)
298 | link.removeAttribute('href')
299 | }, 0)
300 |
301 | link.click()
302 | }
303 |
--------------------------------------------------------------------------------
/src/ProjectedMaterial.js:
--------------------------------------------------------------------------------
1 | import * as THREE from 'three'
2 | import { monkeyPatch } from './three-utils'
3 |
4 | export default class ProjectedMaterial extends THREE.ShaderMaterial {
5 | constructor({
6 | camera,
7 | texture,
8 | color = 0xffffff,
9 | textureScale = 1,
10 | instanced = false,
11 | cover = false,
12 | opacity = 1,
13 | ...options
14 | } = {}) {
15 | if (!texture || !texture.isTexture) {
16 | throw new Error('Invalid texture passed to the ProjectedMaterial')
17 | }
18 |
19 | if (!camera || !camera.isCamera) {
20 | throw new Error('Invalid camera passed to the ProjectedMaterial')
21 | }
22 |
23 | // make sure the camera matrices are updated
24 | camera.updateProjectionMatrix()
25 | camera.updateMatrixWorld()
26 | camera.updateWorldMatrix()
27 |
28 | // get the matrices from the camera so they're fixed in camera's original position
29 | const viewMatrixCamera = camera.matrixWorldInverse.clone()
30 | const projectionMatrixCamera = camera.projectionMatrix.clone()
31 | const modelMatrixCamera = camera.matrixWorld.clone()
32 |
33 | const projPosition = camera.position.clone()
34 |
35 | // scale to keep the image proportions and apply textureScale
36 | const [widthScaled, heightScaled] = computeScaledDimensions(
37 | texture,
38 | camera,
39 | textureScale,
40 | cover
41 | )
42 |
43 | super({
44 | ...options,
45 | lights: true,
46 | uniforms: {
47 | ...THREE.ShaderLib['lambert'].uniforms,
48 | baseColor: { value: new THREE.Color(color) },
49 | texture: { value: texture },
50 | viewMatrixCamera: { type: 'm4', value: viewMatrixCamera },
51 | projectionMatrixCamera: { type: 'm4', value: projectionMatrixCamera },
52 | modelMatrixCamera: { type: 'mat4', value: modelMatrixCamera },
53 | // we will set this later when we will have positioned the object
54 | savedModelMatrix: { type: 'mat4', value: new THREE.Matrix4() },
55 | projPosition: { type: 'v3', value: projPosition },
56 | widthScaled: { value: widthScaled },
57 | heightScaled: { value: heightScaled },
58 | opacity: { value: opacity },
59 | },
60 |
61 | vertexShader: monkeyPatch(THREE.ShaderChunk['meshlambert_vert'], {
62 | header: [
63 | instanced
64 | ? `
65 | attribute vec4 savedModelMatrix0;
66 | attribute vec4 savedModelMatrix1;
67 | attribute vec4 savedModelMatrix2;
68 | attribute vec4 savedModelMatrix3;
69 | `
70 | : `
71 | uniform mat4 savedModelMatrix;
72 | `,
73 | `
74 | uniform mat4 viewMatrixCamera;
75 | uniform mat4 projectionMatrixCamera;
76 | uniform mat4 modelMatrixCamera;
77 |
78 | varying vec4 vWorldPosition;
79 | varying vec3 vNormal;
80 | varying vec4 vTexCoords;
81 | `,
82 | ].join(''),
83 | main: [
84 | instanced
85 | ? `
86 | mat4 savedModelMatrix = mat4(
87 | savedModelMatrix0,
88 | savedModelMatrix1,
89 | savedModelMatrix2,
90 | savedModelMatrix3
91 | );
92 | `
93 | : '',
94 | `
95 | vNormal = mat3(savedModelMatrix) * normal;
96 | vWorldPosition = savedModelMatrix * vec4(position, 1.0);
97 | vTexCoords = projectionMatrixCamera * viewMatrixCamera * vWorldPosition;
98 | `,
99 | ].join(''),
100 | }),
101 |
102 | fragmentShader: monkeyPatch(THREE.ShaderChunk['meshlambert_frag'], {
103 | header: `
104 | uniform vec3 baseColor;
105 | uniform sampler2D texture;
106 | uniform vec3 projPosition;
107 | uniform float widthScaled;
108 | uniform float heightScaled;
109 |
110 | varying vec3 vNormal;
111 | varying vec4 vWorldPosition;
112 | varying vec4 vTexCoords;
113 |
114 | float map(float value, float min1, float max1, float min2, float max2) {
115 | return min2 + (value - min1) * (max2 - min2) / (max1 - min1);
116 | }
117 | `,
118 | 'vec4 diffuseColor = vec4( diffuse, opacity );': `
119 | vec2 uv = (vTexCoords.xy / vTexCoords.w) * 0.5 + 0.5;
120 |
121 | // apply the corrected width and height
122 | uv.x = map(uv.x, 0.0, 1.0, 0.5 - widthScaled / 2.0, 0.5 + widthScaled / 2.0);
123 | uv.y = map(uv.y, 0.0, 1.0, 0.5 - heightScaled / 2.0, 0.5 + heightScaled / 2.0);
124 |
125 | vec4 color = texture2D(texture, uv);
126 |
127 | // this makes sure we don't sample out of the texture
128 | // TODO handle alpha
129 | bool inTexture = (max(uv.x, uv.y) <= 1.0 && min(uv.x, uv.y) >= 0.0);
130 | if (!inTexture) {
131 | color = vec4(baseColor, 1.0);
132 | }
133 |
134 | // this makes sure we don't render also the back of the object
135 | vec3 projectorDirection = normalize(projPosition - vWorldPosition.xyz);
136 | float dotProduct = dot(vNormal, projectorDirection);
137 | if (dotProduct < 0.0) {
138 | color = vec4(baseColor, 1.0);
139 | }
140 |
141 | // opacity from three.js
142 | color.a *= opacity;
143 |
144 | vec4 diffuseColor = color;
145 | `,
146 | }),
147 | })
148 |
149 | // listen on resize if the camera used for the projection
150 | // is the same used to render.
151 | // do this on window resize because there is no way to
152 | // listen for the resize of the renderer
153 | // (or maybe do a requestanimationframe if the camera.aspect changes)
154 | window.addEventListener('resize', () => {
155 | this.uniforms.projectionMatrixCamera.value.copy(camera.projectionMatrix)
156 |
157 | const [widthScaledNew, heightScaledNew] = computeScaledDimensions(
158 | texture,
159 | camera,
160 | textureScale,
161 | cover
162 | )
163 | this.uniforms.widthScaled.value = widthScaledNew
164 | this.uniforms.heightScaled.value = heightScaledNew
165 | })
166 |
167 | this.isProjectedMaterial = true
168 | this.instanced = instanced
169 | }
170 | }
171 |
172 | // scale to keep the image proportions and apply textureScale
173 | function computeScaledDimensions(texture, camera, textureScale, cover) {
174 | const ratio = texture.image.naturalWidth / texture.image.naturalHeight
175 | const ratioCamera = camera.aspect
176 | const widthCamera = 1
177 | const heightCamera = widthCamera * (1 / ratioCamera)
178 | let widthScaled
179 | let heightScaled
180 | if (cover ? ratio > ratioCamera : ratio < ratioCamera) {
181 | const width = heightCamera * ratio
182 | widthScaled = 1 / ((width / widthCamera) * textureScale)
183 | heightScaled = 1 / textureScale
184 | } else {
185 | const height = widthCamera * (1 / ratio)
186 | heightScaled = 1 / ((height / heightCamera) * textureScale)
187 | widthScaled = 1 / textureScale
188 | }
189 |
190 | return [widthScaled, heightScaled]
191 | }
192 |
193 | export function project(mesh) {
194 | if (!mesh.material.isProjectedMaterial) {
195 | throw new Error(`The mesh material must be a ProjectedMaterial`)
196 | }
197 |
198 | // make sure the matrix is updated
199 | mesh.updateMatrixWorld()
200 |
201 | // we save the object model matrix so it's projected relative
202 | // to that position, like a snapshot
203 | mesh.material.uniforms.savedModelMatrix.value.copy(mesh.matrixWorld)
204 | }
205 |
206 | export function projectInstanceAt(index, instancedMesh, matrixWorld) {
207 | if (!instancedMesh.isInstancedMesh) {
208 | throw new Error(`The provided mesh is not an InstancedMesh`)
209 | }
210 |
211 | if (!instancedMesh.material.isProjectedMaterial) {
212 | throw new Error(`The InstancedMesh material must be a ProjectedMaterial`)
213 | }
214 |
215 | if (
216 | !instancedMesh.geometry.attributes.savedModelMatrix0 ||
217 | !instancedMesh.geometry.attributes.savedModelMatrix1 ||
218 | !instancedMesh.geometry.attributes.savedModelMatrix2 ||
219 | !instancedMesh.geometry.attributes.savedModelMatrix3
220 | ) {
221 | throw new Error(
222 | `No allocated data found on the geometry, please call 'allocateProjectionData(geometry)'`
223 | )
224 | }
225 |
226 | if (!instancedMesh.material.instanced) {
227 | throw new Error(`Please pass 'instanced: true' to the ProjectedMaterial`)
228 | }
229 |
230 | instancedMesh.geometry.attributes.savedModelMatrix0.setXYZW(
231 | index,
232 | matrixWorld.elements[0],
233 | matrixWorld.elements[1],
234 | matrixWorld.elements[2],
235 | matrixWorld.elements[3]
236 | )
237 | instancedMesh.geometry.attributes.savedModelMatrix1.setXYZW(
238 | index,
239 | matrixWorld.elements[4],
240 | matrixWorld.elements[5],
241 | matrixWorld.elements[6],
242 | matrixWorld.elements[7]
243 | )
244 | instancedMesh.geometry.attributes.savedModelMatrix2.setXYZW(
245 | index,
246 | matrixWorld.elements[8],
247 | matrixWorld.elements[9],
248 | matrixWorld.elements[10],
249 | matrixWorld.elements[11]
250 | )
251 | instancedMesh.geometry.attributes.savedModelMatrix3.setXYZW(
252 | index,
253 | matrixWorld.elements[12],
254 | matrixWorld.elements[13],
255 | matrixWorld.elements[14],
256 | matrixWorld.elements[15]
257 | )
258 | }
259 |
260 | export function allocateProjectionData(geometry, instancesCount) {
261 | geometry.setAttribute(
262 | 'savedModelMatrix0',
263 | new THREE.InstancedBufferAttribute(new Float32Array(instancesCount * 4), 4)
264 | )
265 | geometry.setAttribute(
266 | 'savedModelMatrix1',
267 | new THREE.InstancedBufferAttribute(new Float32Array(instancesCount * 4), 4)
268 | )
269 | geometry.setAttribute(
270 | 'savedModelMatrix2',
271 | new THREE.InstancedBufferAttribute(new Float32Array(instancesCount * 4), 4)
272 | )
273 | geometry.setAttribute(
274 | 'savedModelMatrix3',
275 | new THREE.InstancedBufferAttribute(new Float32Array(instancesCount * 4), 4)
276 | )
277 | }
278 |
--------------------------------------------------------------------------------