├── .editorconfig
├── .eslintignore
├── .eslintrc
├── .github
└── workflows
│ └── deploy-github-pages.yml
├── .gitignore
├── .prettierignore
├── .prettierrc
├── LICENSE
├── ProjectedMaterial.d.ts
├── README.md
├── build
├── ProjectedMaterial.js
└── ProjectedMaterial.module.js
├── examples
├── 3d-model.html
├── basic.html
├── css
│ └── style.css
├── envmap.html
├── images
│ ├── bigbucksbunny.mp4
│ ├── black-spot.png
│ ├── charles-unsplash.jpg
│ ├── kandao3_blurred.jpg
│ ├── lukasz-szmigiel-unsplash.jpg
│ ├── source.svg
│ ├── three-projected-material-1.png
│ ├── three-projected-material-2.png
│ ├── three-projected-material-3.png
│ ├── three-projected-material-4.png
│ ├── three-projected-material-5.png
│ ├── three-projected-material-6.png
│ └── uv.jpg
├── index.html
├── instancing.html
├── lib
│ ├── Controls.js
│ ├── ProjectedMaterial.module.js
│ ├── WebGLApp.js
│ ├── controls-gui.module.js
│ ├── controls-state.module.js
│ ├── math-utils.js
│ └── three-utils.js
├── models
│ ├── cinema_screen.glb
│ └── suzanne.gltf
├── multiple-projections-instancing.html
├── multiple-projections.html
├── orthographic-camera.html
├── same-camera.html
├── screenshots
│ ├── 3d-model.png
│ ├── basic.png
│ ├── envmap.png
│ ├── instancing.png
│ ├── multiple-projections-instancing.png
│ ├── multiple-projections.png
│ ├── orthographic-camera.png
│ ├── same-camera.png
│ ├── transparency.png
│ └── video.png
├── transparency.html
└── video.html
├── package-lock.json
├── package.json
├── rollup.config.js
├── screenshot.png
├── src
├── ProjectedMaterial.js
└── three-utils.js
└── yarn.lock
/.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 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | build/
3 | examples/lib/controls-state.module.js
4 | examples/lib/controls-gui.module.js
5 | examples/lib/ProjectedMaterial.module.js
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "eslint-config-accurapp"
3 | }
4 |
--------------------------------------------------------------------------------
/.github/workflows/deploy-github-pages.yml:
--------------------------------------------------------------------------------
1 | name: 'Deploy to GitHub Pages'
2 |
3 | on:
4 | push:
5 | branches:
6 | - master
7 |
8 | jobs:
9 | build:
10 | runs-on: ubuntu-latest
11 | steps:
12 | - uses: actions/checkout@v2
13 |
14 | - name: Deploy
15 | uses: peaceiris/actions-gh-pages@v3
16 | with:
17 | github_token: ${{ secrets.GITHUB_TOKEN }}
18 | publish_dir: ./examples
19 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | *.log
3 |
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | package.json
2 | .*
3 | node_modules/
4 | build/
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "printWidth": 100,
3 | "semi": false,
4 | "singleQuote": true,
5 | "trailingComma": "es5"
6 | }
7 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 Marco Fugaro
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/ProjectedMaterial.d.ts:
--------------------------------------------------------------------------------
1 | declare module 'three-projected-material' {
2 | import {
3 | MeshPhysicalMaterial,
4 | PerspectiveCamera,
5 | Texture,
6 | Vector2,
7 | Matrix4,
8 | Vector3,
9 | BufferGeometry,
10 | InstancedMesh,
11 | MeshPhysicalMaterialParameters,
12 | OrthographicCamera,
13 | Mesh,
14 | } from 'three'
15 |
16 | interface ProjectedMaterialParameters extends MeshPhysicalMaterialParameters {
17 | camera?: PerspectiveCamera | OrthographicCamera
18 | texture?: Texture
19 | textureScale?: number
20 | textureOffset?: Vector2
21 | cover?: boolean
22 | }
23 |
24 | export default class ProjectedMaterial extends MeshPhysicalMaterial {
25 | camera: PerspectiveCamera | OrthographicCamera
26 | texture: Texture
27 | textureScale: number
28 | textureOffset: Vector2
29 | cover: boolean
30 |
31 | uniforms: {
32 | projectedTexture: {
33 | value: Texture
34 | }
35 | isTextureLoaded: {
36 | value: boolean
37 | }
38 | isTextureProjected: {
39 | value: boolean
40 | }
41 | backgroundOpacity: {
42 | value: number
43 | }
44 | viewMatrixCamera: {
45 | value: Matrix4
46 | }
47 | projectionMatrixCamera: {
48 | value: Matrix4
49 | }
50 | projPosition: {
51 | value: Vector3
52 | }
53 | projDirection: {
54 | value: Vector3
55 | }
56 | savedModelMatrix: {
57 | value: Matrix4
58 | }
59 | widthScaled: {
60 | value: number
61 | }
62 | heightScaled: {
63 | value: number
64 | }
65 | textureOffset: {
66 | value: Vector2
67 | }
68 | }
69 |
70 | readonly isProjectedMaterial = true
71 |
72 | constructor({
73 | camera,
74 | texture,
75 | textureScale,
76 | textureOffset,
77 | cover,
78 | ...options
79 | }?: ProjectedMaterialParameters)
80 |
81 | project(mesh: Mesh): void
82 |
83 | projectInstanceAt(
84 | index: number,
85 | instancedMesh: InstancedMesh,
86 | matrixWorld: Matrix4,
87 | {
88 | forceCameraSave,
89 | }?: {
90 | forceCameraSave?: boolean | undefined
91 | }
92 | ): void
93 | }
94 |
95 | export function allocateProjectionData(geometry: BufferGeometry, instancesCount: number): void
96 | }
97 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # three-projected-material
2 |
3 | > Three.js Material which lets you do [Texture Projection](https://en.wikipedia.org/wiki/Projective_texture_mapping) on a 3d Model.
4 |
5 |
6 |
7 |
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 from 'three-projected-material'
35 | ```
36 |
37 | or, if you're using CommonJS
38 |
39 | ```js
40 | const ProjectedMaterial = require('three-projected-material').default
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 ProjectedMaterial = window.projectedMaterial.default
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 | textureScale: 0.8, // scale down the texture a bit
57 | textureOffset: new THREE.Vector2(0.1, 0.1), // you can translate the texture if you want
58 | cover: true, // enable background-size: cover behaviour, by default it's like background-size: contain
59 | color: '#ccc', // the color of the object if it's not projected on
60 | roughness: 0.3, // you can pass any other option that belongs to MeshPhysicalMaterial
61 | })
62 | const box = new THREE.Mesh(geometry, material)
63 | webgl.scene.add(box)
64 |
65 | // move the mesh any way you want!
66 | box.rotation.y = -Math.PI / 4
67 |
68 | // and when you're ready project the texture on the box!
69 | material.project(box)
70 | ```
71 |
72 | ProjectedMaterial also supports **instanced meshes** via three.js' [InstancedMesh](https://threejs.org/docs/index.html#api/en/objects/InstancedMesh), and even **multiple projections**. Check out the examples below for a detailed guide!
73 |
74 | ## [Examples](https://three-projected-material.netlify.app/)
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 | ## API Reference
89 |
90 | ### new ProjectedMaterial({ camera, texture, ...others })
91 |
92 | Create a new material to later use for a mesh.
93 |
94 | | Option | Default | Description |
95 | | --------------- | --------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
96 | | `camera` | | The [PerspectiveCamera](https://threejs.org/docs/#api/en/cameras/PerspectiveCamera) the texture will be projected from. |
97 | | `texture` | | The [Texture](https://threejs.org/docs/#api/en/textures/Texture) being projected. |
98 | | `textureScale` | 1 | Make the texture bigger or smaller. |
99 | | `textureOffset` | `new THREE.Vector2()` | Offset the texture in a x or y direction. The unit system goes from 0 to 1, from the bottom left corner to the top right corner of the projector camera frustum. |
100 | | `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/). |
101 | | `...options` | | Other options you pass to any three.js material like `color`, `opacity`, `envMap` and so on. The material is built from a [MeshPhysicalMaterial](https://threejs.org/docs/index.html#api/en/materials/MeshPhysicalMaterial), so you can pass any property of that material and of its parent [MeshStandardMaterial](https://threejs.org/docs/index.html#api/en/materials/MeshStandardMaterial). |
102 |
103 | These properties are exposed as properties of the material, so you can change them later.
104 |
105 | For example, to update the material texture and change its scale:
106 |
107 | ```js
108 | material.texture = newTexture
109 | material.textureScale = 0.8
110 | ```
111 |
112 | ### material.project(mesh)
113 |
114 | Project the texture from the camera on the mesh. With this method we "take a snaphot" of the current mesh and camera position in space. The
115 | After calling this method, you can move the mesh or the camera freely.
116 |
117 | | Option | Description |
118 | | ------ | ---------------------------------------------------- |
119 | | `mesh` | The mesh that has a `ProjectedMaterial` as material. |
120 |
121 | ### allocateProjectionData(geometry, instancesCount)
122 |
123 | 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`.
124 |
125 | This needs to be called before `.projectInstanceAt()`.
126 |
127 | | Option | Description |
128 | | ---------------- | ----------------------------------------------------------------------------- |
129 | | `geometry` | The geometry that will be passed to the `InstancedMesh`. |
130 | | `instancesCount` | The number of instances, the same that will be passed to the `InstancedMesh`. |
131 |
132 | ### material.projectInstanceAt(index, instancedMesh, matrix)
133 |
134 | Do the projection for an [InstancedMesh](https://threejs.org/docs/#api/en/objects/InstancedMesh). Don't forget to call `updateMatrix()` like you do before calling `InstancedMesh.setMatrixAt()`.
135 |
136 | To do projection an an instanced mesh, the geometry needs to be prepared with `allocateProjectionData()` beforehand.
137 |
138 | ```js
139 | dummy.updateMatrix()
140 | projectInstanceAt(i, instancedMesh, dummy.matrix)
141 | ```
142 |
143 | [Link to the full example about instancing](https://three-projected-material.netlify.app/instancing).
144 |
145 | | Option | Description |
146 | | --------------- | ------------------------------------------------------------------------------------------------------------------------ |
147 | | `index` | The index of the instanced element to project. |
148 | | `instancedMesh` | The [InstancedMesh](https://threejs.org/docs/#api/en/objects/InstancedMesh) with a projected material. |
149 | | `matrix` | The `matrix` of the dummy you used to position the instanced mesh element. Be sure to call `.updateMatrix()` beforehand. |
150 |
--------------------------------------------------------------------------------
/build/ProjectedMaterial.js:
--------------------------------------------------------------------------------
1 | (function (global, factory) {
2 | typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('three')) :
3 | typeof define === 'function' && define.amd ? define(['exports', 'three'], factory) :
4 | (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.projectedMaterial = {}, global.THREE));
5 | })(this, (function (exports, THREE) { 'use strict';
6 |
7 | function _interopNamespace(e) {
8 | if (e && e.__esModule) return e;
9 | var n = Object.create(null);
10 | if (e) {
11 | Object.keys(e).forEach(function (k) {
12 | if (k !== 'default') {
13 | var d = Object.getOwnPropertyDescriptor(e, k);
14 | Object.defineProperty(n, k, d.get ? d : {
15 | enumerable: true,
16 | get: function () { return e[k]; }
17 | });
18 | }
19 | });
20 | }
21 | n["default"] = e;
22 | return Object.freeze(n);
23 | }
24 |
25 | var THREE__namespace = /*#__PURE__*/_interopNamespace(THREE);
26 |
27 | var id = 0;
28 |
29 | function _classPrivateFieldLooseKey(name) {
30 | return "__private_" + id++ + "_" + name;
31 | }
32 |
33 | function _classPrivateFieldLooseBase(receiver, privateKey) {
34 | if (!Object.prototype.hasOwnProperty.call(receiver, privateKey)) {
35 | throw new TypeError("attempted to use private field on non-instance");
36 | }
37 |
38 | return receiver;
39 | }
40 |
41 | function monkeyPatch(shader, _ref) {
42 | let {
43 | defines = '',
44 | header = '',
45 | main = '',
46 | ...replaces
47 | } = _ref;
48 | let patchedShader = shader;
49 |
50 | const replaceAll = (str, find, rep) => str.split(find).join(rep);
51 |
52 | Object.keys(replaces).forEach(key => {
53 | patchedShader = replaceAll(patchedShader, key, replaces[key]);
54 | });
55 | patchedShader = patchedShader.replace('void main() {', `
56 | ${header}
57 | void main() {
58 | ${main}
59 | `);
60 | const stringDefines = Object.keys(defines).map(d => `#define ${d} ${defines[d]}`).join('\n');
61 | return `
62 | ${stringDefines}
63 | ${patchedShader}
64 | `;
65 | } // run the callback when the image will be loaded
66 |
67 | function addLoadListener(texture, callback) {
68 | // return if it's already loaded
69 | if (texture.image && texture.image.videoWidth !== 0 && texture.image.videoHeight !== 0) {
70 | return;
71 | }
72 |
73 | const interval = setInterval(() => {
74 | if (texture.image && texture.image.videoWidth !== 0 && texture.image.videoHeight !== 0) {
75 | clearInterval(interval);
76 | return callback(texture);
77 | }
78 | }, 16);
79 | }
80 |
81 | var _camera = /*#__PURE__*/_classPrivateFieldLooseKey("camera");
82 |
83 | var _cover = /*#__PURE__*/_classPrivateFieldLooseKey("cover");
84 |
85 | var _textureScale = /*#__PURE__*/_classPrivateFieldLooseKey("textureScale");
86 |
87 | var _saveCameraProjectionMatrix = /*#__PURE__*/_classPrivateFieldLooseKey("saveCameraProjectionMatrix");
88 |
89 | var _saveDimensions = /*#__PURE__*/_classPrivateFieldLooseKey("saveDimensions");
90 |
91 | var _saveCameraMatrices = /*#__PURE__*/_classPrivateFieldLooseKey("saveCameraMatrices");
92 |
93 | class ProjectedMaterial extends THREE__namespace.MeshPhysicalMaterial {
94 | // internal values... they are exposed via getters
95 | get camera() {
96 | return _classPrivateFieldLooseBase(this, _camera)[_camera];
97 | }
98 |
99 | set camera(camera) {
100 | if (!camera || !camera.isCamera) {
101 | throw new Error('Invalid camera set to the ProjectedMaterial');
102 | }
103 |
104 | _classPrivateFieldLooseBase(this, _camera)[_camera] = camera;
105 |
106 | _classPrivateFieldLooseBase(this, _saveDimensions)[_saveDimensions]();
107 | }
108 |
109 | get texture() {
110 | return this.uniforms.projectedTexture.value;
111 | }
112 |
113 | set texture(texture) {
114 | if (!(texture != null && texture.isTexture)) {
115 | throw new Error('Invalid texture set to the ProjectedMaterial');
116 | }
117 |
118 | this.uniforms.projectedTexture.value = texture;
119 | this.uniforms.isTextureLoaded.value = Boolean(texture.image);
120 |
121 | if (!this.uniforms.isTextureLoaded.value) {
122 | addLoadListener(texture, () => {
123 | this.uniforms.isTextureLoaded.value = true;
124 |
125 | _classPrivateFieldLooseBase(this, _saveDimensions)[_saveDimensions]();
126 | });
127 | } else {
128 | _classPrivateFieldLooseBase(this, _saveDimensions)[_saveDimensions]();
129 | }
130 | }
131 |
132 | get textureScale() {
133 | return _classPrivateFieldLooseBase(this, _textureScale)[_textureScale];
134 | }
135 |
136 | set textureScale(textureScale) {
137 | _classPrivateFieldLooseBase(this, _textureScale)[_textureScale] = textureScale;
138 |
139 | _classPrivateFieldLooseBase(this, _saveDimensions)[_saveDimensions]();
140 | }
141 |
142 | get textureOffset() {
143 | return this.uniforms.textureOffset.value;
144 | }
145 |
146 | set textureOffset(textureOffset) {
147 | this.uniforms.textureOffset.value = textureOffset;
148 | }
149 |
150 | get cover() {
151 | return _classPrivateFieldLooseBase(this, _cover)[_cover];
152 | }
153 |
154 | set cover(cover) {
155 | _classPrivateFieldLooseBase(this, _cover)[_cover] = cover;
156 |
157 | _classPrivateFieldLooseBase(this, _saveDimensions)[_saveDimensions]();
158 | }
159 |
160 | constructor(_temp) {
161 | let {
162 | camera = new THREE__namespace.PerspectiveCamera(),
163 | texture = new THREE__namespace.Texture(),
164 | textureScale = 1,
165 | textureOffset = new THREE__namespace.Vector2(),
166 | cover = false,
167 | ...options
168 | } = _temp === void 0 ? {} : _temp;
169 |
170 | if (!texture.isTexture) {
171 | throw new Error('Invalid texture passed to the ProjectedMaterial');
172 | }
173 |
174 | if (!camera.isCamera) {
175 | throw new Error('Invalid camera passed to the ProjectedMaterial');
176 | }
177 |
178 | super(options);
179 | Object.defineProperty(this, _saveCameraMatrices, {
180 | value: _saveCameraMatrices2
181 | });
182 | Object.defineProperty(this, _saveDimensions, {
183 | value: _saveDimensions2
184 | });
185 | Object.defineProperty(this, _camera, {
186 | writable: true,
187 | value: void 0
188 | });
189 | Object.defineProperty(this, _cover, {
190 | writable: true,
191 | value: void 0
192 | });
193 | Object.defineProperty(this, _textureScale, {
194 | writable: true,
195 | value: void 0
196 | });
197 | Object.defineProperty(this, _saveCameraProjectionMatrix, {
198 | writable: true,
199 | value: () => {
200 | this.uniforms.projectionMatrixCamera.value.copy(this.camera.projectionMatrix);
201 |
202 | _classPrivateFieldLooseBase(this, _saveDimensions)[_saveDimensions]();
203 | }
204 | });
205 | Object.defineProperty(this, 'isProjectedMaterial', {
206 | value: true
207 | }); // save the private variables
208 |
209 | _classPrivateFieldLooseBase(this, _camera)[_camera] = camera;
210 | _classPrivateFieldLooseBase(this, _cover)[_cover] = cover;
211 | _classPrivateFieldLooseBase(this, _textureScale)[_textureScale] = textureScale; // scale to keep the image proportions and apply textureScale
212 |
213 | const [_widthScaled, _heightScaled] = computeScaledDimensions(texture, camera, textureScale, cover);
214 | this.uniforms = {
215 | projectedTexture: {
216 | value: texture
217 | },
218 | // this avoids rendering black if the texture
219 | // hasn't loaded yet
220 | isTextureLoaded: {
221 | value: Boolean(texture.image)
222 | },
223 | // don't show the texture if we haven't called project()
224 | isTextureProjected: {
225 | value: false
226 | },
227 | // if we have multiple materials we want to show the
228 | // background only of the first material
229 | backgroundOpacity: {
230 | value: 1
231 | },
232 | // these will be set on project()
233 | viewMatrixCamera: {
234 | value: new THREE__namespace.Matrix4()
235 | },
236 | projectionMatrixCamera: {
237 | value: new THREE__namespace.Matrix4()
238 | },
239 | projPosition: {
240 | value: new THREE__namespace.Vector3()
241 | },
242 | projDirection: {
243 | value: new THREE__namespace.Vector3(0, 0, -1)
244 | },
245 | // we will set this later when we will have positioned the object
246 | savedModelMatrix: {
247 | value: new THREE__namespace.Matrix4()
248 | },
249 | widthScaled: {
250 | value: _widthScaled
251 | },
252 | heightScaled: {
253 | value: _heightScaled
254 | },
255 | textureOffset: {
256 | value: textureOffset
257 | }
258 | };
259 |
260 | this.onBeforeCompile = shader => {
261 | // expose also the material's uniforms
262 | Object.assign(this.uniforms, shader.uniforms);
263 | shader.uniforms = this.uniforms;
264 |
265 | if (this.camera.isOrthographicCamera) {
266 | shader.defines.ORTHOGRAPHIC = '';
267 | }
268 |
269 | shader.vertexShader = monkeyPatch(shader.vertexShader, {
270 | header:
271 | /* glsl */
272 | `
273 | uniform mat4 viewMatrixCamera;
274 | uniform mat4 projectionMatrixCamera;
275 |
276 | #ifdef USE_INSTANCING
277 | attribute vec4 savedModelMatrix0;
278 | attribute vec4 savedModelMatrix1;
279 | attribute vec4 savedModelMatrix2;
280 | attribute vec4 savedModelMatrix3;
281 | #else
282 | uniform mat4 savedModelMatrix;
283 | #endif
284 |
285 | varying vec3 vSavedNormal;
286 | varying vec4 vTexCoords;
287 | #ifndef ORTHOGRAPHIC
288 | varying vec4 vWorldPosition;
289 | #endif
290 | `,
291 | main:
292 | /* glsl */
293 | `
294 | #ifdef USE_INSTANCING
295 | mat4 savedModelMatrix = mat4(
296 | savedModelMatrix0,
297 | savedModelMatrix1,
298 | savedModelMatrix2,
299 | savedModelMatrix3
300 | );
301 | #endif
302 |
303 | vSavedNormal = mat3(savedModelMatrix) * normal;
304 | vTexCoords = projectionMatrixCamera * viewMatrixCamera * savedModelMatrix * vec4(position, 1.0);
305 | #ifndef ORTHOGRAPHIC
306 | vWorldPosition = savedModelMatrix * vec4(position, 1.0);
307 | #endif
308 | `
309 | });
310 | shader.fragmentShader = monkeyPatch(shader.fragmentShader, {
311 | header:
312 | /* glsl */
313 | `
314 | uniform sampler2D projectedTexture;
315 | uniform bool isTextureLoaded;
316 | uniform bool isTextureProjected;
317 | uniform float backgroundOpacity;
318 | uniform vec3 projPosition;
319 | uniform vec3 projDirection;
320 | uniform float widthScaled;
321 | uniform float heightScaled;
322 | uniform vec2 textureOffset;
323 |
324 | varying vec3 vSavedNormal;
325 | varying vec4 vTexCoords;
326 | #ifndef ORTHOGRAPHIC
327 | varying vec4 vWorldPosition;
328 | #endif
329 |
330 | float mapRange(float value, float min1, float max1, float min2, float max2) {
331 | return min2 + (value - min1) * (max2 - min2) / (max1 - min1);
332 | }
333 | `,
334 | 'vec4 diffuseColor = vec4( diffuse, opacity );':
335 | /* glsl */
336 | `
337 | // clamp the w to make sure we don't project behind
338 | float w = max(vTexCoords.w, 0.0);
339 |
340 | vec2 uv = (vTexCoords.xy / w) * 0.5 + 0.5;
341 |
342 | uv += textureOffset;
343 |
344 | // apply the corrected width and height
345 | uv.x = mapRange(uv.x, 0.0, 1.0, 0.5 - widthScaled / 2.0, 0.5 + widthScaled / 2.0);
346 | uv.y = mapRange(uv.y, 0.0, 1.0, 0.5 - heightScaled / 2.0, 0.5 + heightScaled / 2.0);
347 |
348 | // this makes sure we don't sample out of the texture
349 | bool isInTexture = (max(uv.x, uv.y) <= 1.0 && min(uv.x, uv.y) >= 0.0);
350 |
351 | // this makes sure we don't render also the back of the object
352 | #ifdef ORTHOGRAPHIC
353 | vec3 projectorDirection = projDirection;
354 | #else
355 | vec3 projectorDirection = normalize(projPosition - vWorldPosition.xyz);
356 | #endif
357 | float dotProduct = dot(vSavedNormal, projectorDirection);
358 | bool isFacingProjector = dotProduct > 0.0000001;
359 |
360 |
361 | vec4 diffuseColor = vec4(diffuse, opacity * backgroundOpacity);
362 |
363 | if (isFacingProjector && isInTexture && isTextureLoaded && isTextureProjected) {
364 | vec4 textureColor = texture2D(projectedTexture, uv);
365 |
366 | // apply the material opacity
367 | textureColor.a *= opacity;
368 |
369 | // https://learnopengl.com/Advanced-OpenGL/Blending
370 | diffuseColor = textureColor * textureColor.a + diffuseColor * (1.0 - textureColor.a);
371 | }
372 | `
373 | });
374 | }; // Listen on resize if the camera used for the projection
375 | // is the same used to render.
376 | // We do this on window resize because there is no way to
377 | // listen for the resize of the renderer
378 |
379 |
380 | window.addEventListener('resize', _classPrivateFieldLooseBase(this, _saveCameraProjectionMatrix)[_saveCameraProjectionMatrix]); // If the image texture passed hasn't loaded yet,
381 | // wait for it to load and compute the correct proportions.
382 | // This avoids rendering black while the texture is loading
383 |
384 | addLoadListener(texture, () => {
385 | this.uniforms.isTextureLoaded.value = true;
386 |
387 | _classPrivateFieldLooseBase(this, _saveDimensions)[_saveDimensions]();
388 | });
389 | }
390 |
391 | project(mesh) {
392 | if (!(Array.isArray(mesh.material) ? mesh.material.every(m => m.isProjectedMaterial) : mesh.material.isProjectedMaterial)) {
393 | throw new Error(`The mesh material must be a ProjectedMaterial`);
394 | }
395 |
396 | if (!(Array.isArray(mesh.material) ? mesh.material.some(m => m === this) : mesh.material === this)) {
397 | throw new Error(`The provided mesh doesn't have the same material as where project() has been called from`);
398 | } // make sure the matrix is updated
399 |
400 |
401 | mesh.updateWorldMatrix(true, false); // we save the object model matrix so it's projected relative
402 | // to that position, like a snapshot
403 |
404 | this.uniforms.savedModelMatrix.value.copy(mesh.matrixWorld); // if the material is not the first, output just the texture
405 |
406 | if (Array.isArray(mesh.material)) {
407 | const materialIndex = mesh.material.indexOf(this);
408 |
409 | if (!mesh.material[materialIndex].transparent) {
410 | throw new Error(`You have to pass "transparent: true" to the ProjectedMaterial if you're working with multiple materials.`);
411 | }
412 |
413 | if (materialIndex > 0) {
414 | this.uniforms.backgroundOpacity.value = 0;
415 | }
416 | } // persist also the current camera position and matrices
417 |
418 |
419 | _classPrivateFieldLooseBase(this, _saveCameraMatrices)[_saveCameraMatrices]();
420 | }
421 |
422 | projectInstanceAt(index, instancedMesh, matrixWorld, _temp2) {
423 | let {
424 | forceCameraSave = false
425 | } = _temp2 === void 0 ? {} : _temp2;
426 |
427 | if (!instancedMesh.isInstancedMesh) {
428 | throw new Error(`The provided mesh is not an InstancedMesh`);
429 | }
430 |
431 | if (!(Array.isArray(instancedMesh.material) ? instancedMesh.material.every(m => m.isProjectedMaterial) : instancedMesh.material.isProjectedMaterial)) {
432 | throw new Error(`The InstancedMesh material must be a ProjectedMaterial`);
433 | }
434 |
435 | if (!(Array.isArray(instancedMesh.material) ? instancedMesh.material.some(m => m === this) : instancedMesh.material === this)) {
436 | throw new Error(`The provided InstancedMeshhave't i samenclude thas e material where project() has been called from`);
437 | }
438 |
439 | if (!instancedMesh.geometry.attributes[`savedModelMatrix0`] || !instancedMesh.geometry.attributes[`savedModelMatrix1`] || !instancedMesh.geometry.attributes[`savedModelMatrix2`] || !instancedMesh.geometry.attributes[`savedModelMatrix3`]) {
440 | throw new Error(`No allocated data found on the geometry, please call 'allocateProjectionData(geometry, instancesCount)'`);
441 | }
442 |
443 | instancedMesh.geometry.attributes[`savedModelMatrix0`].setXYZW(index, matrixWorld.elements[0], matrixWorld.elements[1], matrixWorld.elements[2], matrixWorld.elements[3]);
444 | instancedMesh.geometry.attributes[`savedModelMatrix1`].setXYZW(index, matrixWorld.elements[4], matrixWorld.elements[5], matrixWorld.elements[6], matrixWorld.elements[7]);
445 | instancedMesh.geometry.attributes[`savedModelMatrix2`].setXYZW(index, matrixWorld.elements[8], matrixWorld.elements[9], matrixWorld.elements[10], matrixWorld.elements[11]);
446 | instancedMesh.geometry.attributes[`savedModelMatrix3`].setXYZW(index, matrixWorld.elements[12], matrixWorld.elements[13], matrixWorld.elements[14], matrixWorld.elements[15]); // if the material is not the first, output just the texture
447 |
448 | if (Array.isArray(instancedMesh.material)) {
449 | const materialIndex = instancedMesh.material.indexOf(this);
450 |
451 | if (!instancedMesh.material[materialIndex].transparent) {
452 | throw new Error(`You have to pass "transparent: true" to the ProjectedMaterial if you're working with multiple materials.`);
453 | }
454 |
455 | if (materialIndex > 0) {
456 | this.uniforms.backgroundOpacity.value = 0;
457 | }
458 | } // persist the current camera position and matrices
459 | // only if it's the first instance since most surely
460 | // in all other instances the camera won't change
461 |
462 |
463 | if (index === 0 || forceCameraSave) {
464 | _classPrivateFieldLooseBase(this, _saveCameraMatrices)[_saveCameraMatrices]();
465 | }
466 | }
467 |
468 | copy(source) {
469 | super.copy(source);
470 | this.camera = source.camera;
471 | this.texture = source.texture;
472 | this.textureScale = source.textureScale;
473 | this.textureOffset = source.textureOffset;
474 | this.cover = source.cover;
475 | return this;
476 | }
477 |
478 | dispose() {
479 | super.dispose();
480 | window.removeEventListener('resize', _classPrivateFieldLooseBase(this, _saveCameraProjectionMatrix)[_saveCameraProjectionMatrix]);
481 | }
482 |
483 | } // get camera ratio from different types of cameras
484 |
485 | function _saveDimensions2() {
486 | const [widthScaled, heightScaled] = computeScaledDimensions(this.texture, this.camera, this.textureScale, this.cover);
487 | this.uniforms.widthScaled.value = widthScaled;
488 | this.uniforms.heightScaled.value = heightScaled;
489 | }
490 |
491 | function _saveCameraMatrices2() {
492 | // make sure the camera matrices are updated
493 | this.camera.updateProjectionMatrix();
494 | this.camera.updateMatrixWorld();
495 | this.camera.updateWorldMatrix(); // update the uniforms from the camera so they're
496 | // fixed in the camera's position at the projection time
497 |
498 | const viewMatrixCamera = this.camera.matrixWorldInverse;
499 | const projectionMatrixCamera = this.camera.projectionMatrix;
500 | const modelMatrixCamera = this.camera.matrixWorld;
501 | this.uniforms.viewMatrixCamera.value.copy(viewMatrixCamera);
502 | this.uniforms.projectionMatrixCamera.value.copy(projectionMatrixCamera);
503 | this.uniforms.projPosition.value.copy(this.camera.position);
504 | this.uniforms.projDirection.value.set(0, 0, 1).applyMatrix4(modelMatrixCamera); // tell the shader we've projected
505 |
506 | this.uniforms.isTextureProjected.value = true;
507 | }
508 |
509 | function getCameraRatio(camera) {
510 | switch (camera.type) {
511 | case 'PerspectiveCamera':
512 | {
513 | return camera.aspect;
514 | }
515 |
516 | case 'OrthographicCamera':
517 | {
518 | const width = Math.abs(camera.right - camera.left);
519 | const height = Math.abs(camera.top - camera.bottom);
520 | return width / height;
521 | }
522 |
523 | default:
524 | {
525 | throw new Error(`${camera.type} is currently not supported in ProjectedMaterial`);
526 | }
527 | }
528 | } // scale to keep the image proportions and apply textureScale
529 |
530 |
531 | function computeScaledDimensions(texture, camera, textureScale, cover) {
532 | // return some default values if the image hasn't loaded yet
533 | if (!texture.image) {
534 | return [1, 1];
535 | } // return if it's a video and if the video hasn't loaded yet
536 |
537 |
538 | if (texture.image.videoWidth === 0 && texture.image.videoHeight === 0) {
539 | return [1, 1];
540 | }
541 |
542 | const sourceWidth = texture.image.naturalWidth || texture.image.videoWidth || texture.image.clientWidth;
543 | const sourceHeight = texture.image.naturalHeight || texture.image.videoHeight || texture.image.clientHeight;
544 | const ratio = sourceWidth / sourceHeight;
545 | const ratioCamera = getCameraRatio(camera);
546 | const widthCamera = 1;
547 | const heightCamera = widthCamera * (1 / ratioCamera);
548 | let widthScaled;
549 | let heightScaled;
550 |
551 | if (cover ? ratio > ratioCamera : ratio < ratioCamera) {
552 | const width = heightCamera * ratio;
553 | widthScaled = 1 / (width / widthCamera * textureScale);
554 | heightScaled = 1 / textureScale;
555 | } else {
556 | const height = widthCamera * (1 / ratio);
557 | heightScaled = 1 / (height / heightCamera * textureScale);
558 | widthScaled = 1 / textureScale;
559 | }
560 |
561 | return [widthScaled, heightScaled];
562 | }
563 |
564 | function allocateProjectionData(geometry, instancesCount) {
565 | geometry.setAttribute(`savedModelMatrix0`, new THREE__namespace.InstancedBufferAttribute(new Float32Array(instancesCount * 4), 4));
566 | geometry.setAttribute(`savedModelMatrix1`, new THREE__namespace.InstancedBufferAttribute(new Float32Array(instancesCount * 4), 4));
567 | geometry.setAttribute(`savedModelMatrix2`, new THREE__namespace.InstancedBufferAttribute(new Float32Array(instancesCount * 4), 4));
568 | geometry.setAttribute(`savedModelMatrix3`, new THREE__namespace.InstancedBufferAttribute(new Float32Array(instancesCount * 4), 4));
569 | }
570 |
571 | exports.allocateProjectionData = allocateProjectionData;
572 | exports["default"] = ProjectedMaterial;
573 |
574 | Object.defineProperty(exports, '__esModule', { value: true });
575 |
576 | }));
577 |
--------------------------------------------------------------------------------
/build/ProjectedMaterial.module.js:
--------------------------------------------------------------------------------
1 | import * as THREE from 'three';
2 |
3 | var id = 0;
4 |
5 | function _classPrivateFieldLooseKey(name) {
6 | return "__private_" + id++ + "_" + name;
7 | }
8 |
9 | function _classPrivateFieldLooseBase(receiver, privateKey) {
10 | if (!Object.prototype.hasOwnProperty.call(receiver, privateKey)) {
11 | throw new TypeError("attempted to use private field on non-instance");
12 | }
13 |
14 | return receiver;
15 | }
16 |
17 | function monkeyPatch(shader, _ref) {
18 | let {
19 | defines = '',
20 | header = '',
21 | main = '',
22 | ...replaces
23 | } = _ref;
24 | let patchedShader = shader;
25 |
26 | const replaceAll = (str, find, rep) => str.split(find).join(rep);
27 |
28 | Object.keys(replaces).forEach(key => {
29 | patchedShader = replaceAll(patchedShader, key, replaces[key]);
30 | });
31 | patchedShader = patchedShader.replace('void main() {', `
32 | ${header}
33 | void main() {
34 | ${main}
35 | `);
36 | const stringDefines = Object.keys(defines).map(d => `#define ${d} ${defines[d]}`).join('\n');
37 | return `
38 | ${stringDefines}
39 | ${patchedShader}
40 | `;
41 | } // run the callback when the image will be loaded
42 |
43 | function addLoadListener(texture, callback) {
44 | // return if it's already loaded
45 | if (texture.image && texture.image.videoWidth !== 0 && texture.image.videoHeight !== 0) {
46 | return;
47 | }
48 |
49 | const interval = setInterval(() => {
50 | if (texture.image && texture.image.videoWidth !== 0 && texture.image.videoHeight !== 0) {
51 | clearInterval(interval);
52 | return callback(texture);
53 | }
54 | }, 16);
55 | }
56 |
57 | var _camera = /*#__PURE__*/_classPrivateFieldLooseKey("camera");
58 |
59 | var _cover = /*#__PURE__*/_classPrivateFieldLooseKey("cover");
60 |
61 | var _textureScale = /*#__PURE__*/_classPrivateFieldLooseKey("textureScale");
62 |
63 | var _saveCameraProjectionMatrix = /*#__PURE__*/_classPrivateFieldLooseKey("saveCameraProjectionMatrix");
64 |
65 | var _saveDimensions = /*#__PURE__*/_classPrivateFieldLooseKey("saveDimensions");
66 |
67 | var _saveCameraMatrices = /*#__PURE__*/_classPrivateFieldLooseKey("saveCameraMatrices");
68 |
69 | class ProjectedMaterial extends THREE.MeshPhysicalMaterial {
70 | // internal values... they are exposed via getters
71 | get camera() {
72 | return _classPrivateFieldLooseBase(this, _camera)[_camera];
73 | }
74 |
75 | set camera(camera) {
76 | if (!camera || !camera.isCamera) {
77 | throw new Error('Invalid camera set to the ProjectedMaterial');
78 | }
79 |
80 | _classPrivateFieldLooseBase(this, _camera)[_camera] = camera;
81 |
82 | _classPrivateFieldLooseBase(this, _saveDimensions)[_saveDimensions]();
83 | }
84 |
85 | get texture() {
86 | return this.uniforms.projectedTexture.value;
87 | }
88 |
89 | set texture(texture) {
90 | if (!(texture != null && texture.isTexture)) {
91 | throw new Error('Invalid texture set to the ProjectedMaterial');
92 | }
93 |
94 | this.uniforms.projectedTexture.value = texture;
95 | this.uniforms.isTextureLoaded.value = Boolean(texture.image);
96 |
97 | if (!this.uniforms.isTextureLoaded.value) {
98 | addLoadListener(texture, () => {
99 | this.uniforms.isTextureLoaded.value = true;
100 |
101 | _classPrivateFieldLooseBase(this, _saveDimensions)[_saveDimensions]();
102 | });
103 | } else {
104 | _classPrivateFieldLooseBase(this, _saveDimensions)[_saveDimensions]();
105 | }
106 | }
107 |
108 | get textureScale() {
109 | return _classPrivateFieldLooseBase(this, _textureScale)[_textureScale];
110 | }
111 |
112 | set textureScale(textureScale) {
113 | _classPrivateFieldLooseBase(this, _textureScale)[_textureScale] = textureScale;
114 |
115 | _classPrivateFieldLooseBase(this, _saveDimensions)[_saveDimensions]();
116 | }
117 |
118 | get textureOffset() {
119 | return this.uniforms.textureOffset.value;
120 | }
121 |
122 | set textureOffset(textureOffset) {
123 | this.uniforms.textureOffset.value = textureOffset;
124 | }
125 |
126 | get cover() {
127 | return _classPrivateFieldLooseBase(this, _cover)[_cover];
128 | }
129 |
130 | set cover(cover) {
131 | _classPrivateFieldLooseBase(this, _cover)[_cover] = cover;
132 |
133 | _classPrivateFieldLooseBase(this, _saveDimensions)[_saveDimensions]();
134 | }
135 |
136 | constructor(_temp) {
137 | let {
138 | camera = new THREE.PerspectiveCamera(),
139 | texture = new THREE.Texture(),
140 | textureScale = 1,
141 | textureOffset = new THREE.Vector2(),
142 | cover = false,
143 | ...options
144 | } = _temp === void 0 ? {} : _temp;
145 |
146 | if (!texture.isTexture) {
147 | throw new Error('Invalid texture passed to the ProjectedMaterial');
148 | }
149 |
150 | if (!camera.isCamera) {
151 | throw new Error('Invalid camera passed to the ProjectedMaterial');
152 | }
153 |
154 | super(options);
155 | Object.defineProperty(this, _saveCameraMatrices, {
156 | value: _saveCameraMatrices2
157 | });
158 | Object.defineProperty(this, _saveDimensions, {
159 | value: _saveDimensions2
160 | });
161 | Object.defineProperty(this, _camera, {
162 | writable: true,
163 | value: void 0
164 | });
165 | Object.defineProperty(this, _cover, {
166 | writable: true,
167 | value: void 0
168 | });
169 | Object.defineProperty(this, _textureScale, {
170 | writable: true,
171 | value: void 0
172 | });
173 | Object.defineProperty(this, _saveCameraProjectionMatrix, {
174 | writable: true,
175 | value: () => {
176 | this.uniforms.projectionMatrixCamera.value.copy(this.camera.projectionMatrix);
177 |
178 | _classPrivateFieldLooseBase(this, _saveDimensions)[_saveDimensions]();
179 | }
180 | });
181 | Object.defineProperty(this, 'isProjectedMaterial', {
182 | value: true
183 | }); // save the private variables
184 |
185 | _classPrivateFieldLooseBase(this, _camera)[_camera] = camera;
186 | _classPrivateFieldLooseBase(this, _cover)[_cover] = cover;
187 | _classPrivateFieldLooseBase(this, _textureScale)[_textureScale] = textureScale; // scale to keep the image proportions and apply textureScale
188 |
189 | const [_widthScaled, _heightScaled] = computeScaledDimensions(texture, camera, textureScale, cover);
190 | this.uniforms = {
191 | projectedTexture: {
192 | value: texture
193 | },
194 | // this avoids rendering black if the texture
195 | // hasn't loaded yet
196 | isTextureLoaded: {
197 | value: Boolean(texture.image)
198 | },
199 | // don't show the texture if we haven't called project()
200 | isTextureProjected: {
201 | value: false
202 | },
203 | // if we have multiple materials we want to show the
204 | // background only of the first material
205 | backgroundOpacity: {
206 | value: 1
207 | },
208 | // these will be set on project()
209 | viewMatrixCamera: {
210 | value: new THREE.Matrix4()
211 | },
212 | projectionMatrixCamera: {
213 | value: new THREE.Matrix4()
214 | },
215 | projPosition: {
216 | value: new THREE.Vector3()
217 | },
218 | projDirection: {
219 | value: new THREE.Vector3(0, 0, -1)
220 | },
221 | // we will set this later when we will have positioned the object
222 | savedModelMatrix: {
223 | value: new THREE.Matrix4()
224 | },
225 | widthScaled: {
226 | value: _widthScaled
227 | },
228 | heightScaled: {
229 | value: _heightScaled
230 | },
231 | textureOffset: {
232 | value: textureOffset
233 | }
234 | };
235 |
236 | this.onBeforeCompile = shader => {
237 | // expose also the material's uniforms
238 | Object.assign(this.uniforms, shader.uniforms);
239 | shader.uniforms = this.uniforms;
240 |
241 | if (this.camera.isOrthographicCamera) {
242 | shader.defines.ORTHOGRAPHIC = '';
243 | }
244 |
245 | shader.vertexShader = monkeyPatch(shader.vertexShader, {
246 | header:
247 | /* glsl */
248 | `
249 | uniform mat4 viewMatrixCamera;
250 | uniform mat4 projectionMatrixCamera;
251 |
252 | #ifdef USE_INSTANCING
253 | attribute vec4 savedModelMatrix0;
254 | attribute vec4 savedModelMatrix1;
255 | attribute vec4 savedModelMatrix2;
256 | attribute vec4 savedModelMatrix3;
257 | #else
258 | uniform mat4 savedModelMatrix;
259 | #endif
260 |
261 | varying vec3 vSavedNormal;
262 | varying vec4 vTexCoords;
263 | #ifndef ORTHOGRAPHIC
264 | varying vec4 vWorldPosition;
265 | #endif
266 | `,
267 | main:
268 | /* glsl */
269 | `
270 | #ifdef USE_INSTANCING
271 | mat4 savedModelMatrix = mat4(
272 | savedModelMatrix0,
273 | savedModelMatrix1,
274 | savedModelMatrix2,
275 | savedModelMatrix3
276 | );
277 | #endif
278 |
279 | vSavedNormal = mat3(savedModelMatrix) * normal;
280 | vTexCoords = projectionMatrixCamera * viewMatrixCamera * savedModelMatrix * vec4(position, 1.0);
281 | #ifndef ORTHOGRAPHIC
282 | vWorldPosition = savedModelMatrix * vec4(position, 1.0);
283 | #endif
284 | `
285 | });
286 | shader.fragmentShader = monkeyPatch(shader.fragmentShader, {
287 | header:
288 | /* glsl */
289 | `
290 | uniform sampler2D projectedTexture;
291 | uniform bool isTextureLoaded;
292 | uniform bool isTextureProjected;
293 | uniform float backgroundOpacity;
294 | uniform vec3 projPosition;
295 | uniform vec3 projDirection;
296 | uniform float widthScaled;
297 | uniform float heightScaled;
298 | uniform vec2 textureOffset;
299 |
300 | varying vec3 vSavedNormal;
301 | varying vec4 vTexCoords;
302 | #ifndef ORTHOGRAPHIC
303 | varying vec4 vWorldPosition;
304 | #endif
305 |
306 | float mapRange(float value, float min1, float max1, float min2, float max2) {
307 | return min2 + (value - min1) * (max2 - min2) / (max1 - min1);
308 | }
309 | `,
310 | 'vec4 diffuseColor = vec4( diffuse, opacity );':
311 | /* glsl */
312 | `
313 | // clamp the w to make sure we don't project behind
314 | float w = max(vTexCoords.w, 0.0);
315 |
316 | vec2 uv = (vTexCoords.xy / w) * 0.5 + 0.5;
317 |
318 | uv += textureOffset;
319 |
320 | // apply the corrected width and height
321 | uv.x = mapRange(uv.x, 0.0, 1.0, 0.5 - widthScaled / 2.0, 0.5 + widthScaled / 2.0);
322 | uv.y = mapRange(uv.y, 0.0, 1.0, 0.5 - heightScaled / 2.0, 0.5 + heightScaled / 2.0);
323 |
324 | // this makes sure we don't sample out of the texture
325 | bool isInTexture = (max(uv.x, uv.y) <= 1.0 && min(uv.x, uv.y) >= 0.0);
326 |
327 | // this makes sure we don't render also the back of the object
328 | #ifdef ORTHOGRAPHIC
329 | vec3 projectorDirection = projDirection;
330 | #else
331 | vec3 projectorDirection = normalize(projPosition - vWorldPosition.xyz);
332 | #endif
333 | float dotProduct = dot(vSavedNormal, projectorDirection);
334 | bool isFacingProjector = dotProduct > 0.0000001;
335 |
336 |
337 | vec4 diffuseColor = vec4(diffuse, opacity * backgroundOpacity);
338 |
339 | if (isFacingProjector && isInTexture && isTextureLoaded && isTextureProjected) {
340 | vec4 textureColor = texture2D(projectedTexture, uv);
341 |
342 | // apply the material opacity
343 | textureColor.a *= opacity;
344 |
345 | // https://learnopengl.com/Advanced-OpenGL/Blending
346 | diffuseColor = textureColor * textureColor.a + diffuseColor * (1.0 - textureColor.a);
347 | }
348 | `
349 | });
350 | }; // Listen on resize if the camera used for the projection
351 | // is the same used to render.
352 | // We do this on window resize because there is no way to
353 | // listen for the resize of the renderer
354 |
355 |
356 | window.addEventListener('resize', _classPrivateFieldLooseBase(this, _saveCameraProjectionMatrix)[_saveCameraProjectionMatrix]); // If the image texture passed hasn't loaded yet,
357 | // wait for it to load and compute the correct proportions.
358 | // This avoids rendering black while the texture is loading
359 |
360 | addLoadListener(texture, () => {
361 | this.uniforms.isTextureLoaded.value = true;
362 |
363 | _classPrivateFieldLooseBase(this, _saveDimensions)[_saveDimensions]();
364 | });
365 | }
366 |
367 | project(mesh) {
368 | if (!(Array.isArray(mesh.material) ? mesh.material.every(m => m.isProjectedMaterial) : mesh.material.isProjectedMaterial)) {
369 | throw new Error(`The mesh material must be a ProjectedMaterial`);
370 | }
371 |
372 | if (!(Array.isArray(mesh.material) ? mesh.material.some(m => m === this) : mesh.material === this)) {
373 | throw new Error(`The provided mesh doesn't have the same material as where project() has been called from`);
374 | } // make sure the matrix is updated
375 |
376 |
377 | mesh.updateWorldMatrix(true, false); // we save the object model matrix so it's projected relative
378 | // to that position, like a snapshot
379 |
380 | this.uniforms.savedModelMatrix.value.copy(mesh.matrixWorld); // if the material is not the first, output just the texture
381 |
382 | if (Array.isArray(mesh.material)) {
383 | const materialIndex = mesh.material.indexOf(this);
384 |
385 | if (!mesh.material[materialIndex].transparent) {
386 | throw new Error(`You have to pass "transparent: true" to the ProjectedMaterial if you're working with multiple materials.`);
387 | }
388 |
389 | if (materialIndex > 0) {
390 | this.uniforms.backgroundOpacity.value = 0;
391 | }
392 | } // persist also the current camera position and matrices
393 |
394 |
395 | _classPrivateFieldLooseBase(this, _saveCameraMatrices)[_saveCameraMatrices]();
396 | }
397 |
398 | projectInstanceAt(index, instancedMesh, matrixWorld, _temp2) {
399 | let {
400 | forceCameraSave = false
401 | } = _temp2 === void 0 ? {} : _temp2;
402 |
403 | if (!instancedMesh.isInstancedMesh) {
404 | throw new Error(`The provided mesh is not an InstancedMesh`);
405 | }
406 |
407 | if (!(Array.isArray(instancedMesh.material) ? instancedMesh.material.every(m => m.isProjectedMaterial) : instancedMesh.material.isProjectedMaterial)) {
408 | throw new Error(`The InstancedMesh material must be a ProjectedMaterial`);
409 | }
410 |
411 | if (!(Array.isArray(instancedMesh.material) ? instancedMesh.material.some(m => m === this) : instancedMesh.material === this)) {
412 | throw new Error(`The provided InstancedMeshhave't i samenclude thas e material where project() has been called from`);
413 | }
414 |
415 | if (!instancedMesh.geometry.attributes[`savedModelMatrix0`] || !instancedMesh.geometry.attributes[`savedModelMatrix1`] || !instancedMesh.geometry.attributes[`savedModelMatrix2`] || !instancedMesh.geometry.attributes[`savedModelMatrix3`]) {
416 | throw new Error(`No allocated data found on the geometry, please call 'allocateProjectionData(geometry, instancesCount)'`);
417 | }
418 |
419 | instancedMesh.geometry.attributes[`savedModelMatrix0`].setXYZW(index, matrixWorld.elements[0], matrixWorld.elements[1], matrixWorld.elements[2], matrixWorld.elements[3]);
420 | instancedMesh.geometry.attributes[`savedModelMatrix1`].setXYZW(index, matrixWorld.elements[4], matrixWorld.elements[5], matrixWorld.elements[6], matrixWorld.elements[7]);
421 | instancedMesh.geometry.attributes[`savedModelMatrix2`].setXYZW(index, matrixWorld.elements[8], matrixWorld.elements[9], matrixWorld.elements[10], matrixWorld.elements[11]);
422 | instancedMesh.geometry.attributes[`savedModelMatrix3`].setXYZW(index, matrixWorld.elements[12], matrixWorld.elements[13], matrixWorld.elements[14], matrixWorld.elements[15]); // if the material is not the first, output just the texture
423 |
424 | if (Array.isArray(instancedMesh.material)) {
425 | const materialIndex = instancedMesh.material.indexOf(this);
426 |
427 | if (!instancedMesh.material[materialIndex].transparent) {
428 | throw new Error(`You have to pass "transparent: true" to the ProjectedMaterial if you're working with multiple materials.`);
429 | }
430 |
431 | if (materialIndex > 0) {
432 | this.uniforms.backgroundOpacity.value = 0;
433 | }
434 | } // persist the current camera position and matrices
435 | // only if it's the first instance since most surely
436 | // in all other instances the camera won't change
437 |
438 |
439 | if (index === 0 || forceCameraSave) {
440 | _classPrivateFieldLooseBase(this, _saveCameraMatrices)[_saveCameraMatrices]();
441 | }
442 | }
443 |
444 | copy(source) {
445 | super.copy(source);
446 | this.camera = source.camera;
447 | this.texture = source.texture;
448 | this.textureScale = source.textureScale;
449 | this.textureOffset = source.textureOffset;
450 | this.cover = source.cover;
451 | return this;
452 | }
453 |
454 | dispose() {
455 | super.dispose();
456 | window.removeEventListener('resize', _classPrivateFieldLooseBase(this, _saveCameraProjectionMatrix)[_saveCameraProjectionMatrix]);
457 | }
458 |
459 | } // get camera ratio from different types of cameras
460 |
461 | function _saveDimensions2() {
462 | const [widthScaled, heightScaled] = computeScaledDimensions(this.texture, this.camera, this.textureScale, this.cover);
463 | this.uniforms.widthScaled.value = widthScaled;
464 | this.uniforms.heightScaled.value = heightScaled;
465 | }
466 |
467 | function _saveCameraMatrices2() {
468 | // make sure the camera matrices are updated
469 | this.camera.updateProjectionMatrix();
470 | this.camera.updateMatrixWorld();
471 | this.camera.updateWorldMatrix(); // update the uniforms from the camera so they're
472 | // fixed in the camera's position at the projection time
473 |
474 | const viewMatrixCamera = this.camera.matrixWorldInverse;
475 | const projectionMatrixCamera = this.camera.projectionMatrix;
476 | const modelMatrixCamera = this.camera.matrixWorld;
477 | this.uniforms.viewMatrixCamera.value.copy(viewMatrixCamera);
478 | this.uniforms.projectionMatrixCamera.value.copy(projectionMatrixCamera);
479 | this.uniforms.projPosition.value.copy(this.camera.position);
480 | this.uniforms.projDirection.value.set(0, 0, 1).applyMatrix4(modelMatrixCamera); // tell the shader we've projected
481 |
482 | this.uniforms.isTextureProjected.value = true;
483 | }
484 |
485 | function getCameraRatio(camera) {
486 | switch (camera.type) {
487 | case 'PerspectiveCamera':
488 | {
489 | return camera.aspect;
490 | }
491 |
492 | case 'OrthographicCamera':
493 | {
494 | const width = Math.abs(camera.right - camera.left);
495 | const height = Math.abs(camera.top - camera.bottom);
496 | return width / height;
497 | }
498 |
499 | default:
500 | {
501 | throw new Error(`${camera.type} is currently not supported in ProjectedMaterial`);
502 | }
503 | }
504 | } // scale to keep the image proportions and apply textureScale
505 |
506 |
507 | function computeScaledDimensions(texture, camera, textureScale, cover) {
508 | // return some default values if the image hasn't loaded yet
509 | if (!texture.image) {
510 | return [1, 1];
511 | } // return if it's a video and if the video hasn't loaded yet
512 |
513 |
514 | if (texture.image.videoWidth === 0 && texture.image.videoHeight === 0) {
515 | return [1, 1];
516 | }
517 |
518 | const sourceWidth = texture.image.naturalWidth || texture.image.videoWidth || texture.image.clientWidth;
519 | const sourceHeight = texture.image.naturalHeight || texture.image.videoHeight || texture.image.clientHeight;
520 | const ratio = sourceWidth / sourceHeight;
521 | const ratioCamera = getCameraRatio(camera);
522 | const widthCamera = 1;
523 | const heightCamera = widthCamera * (1 / ratioCamera);
524 | let widthScaled;
525 | let heightScaled;
526 |
527 | if (cover ? ratio > ratioCamera : ratio < ratioCamera) {
528 | const width = heightCamera * ratio;
529 | widthScaled = 1 / (width / widthCamera * textureScale);
530 | heightScaled = 1 / textureScale;
531 | } else {
532 | const height = widthCamera * (1 / ratio);
533 | heightScaled = 1 / (height / heightCamera * textureScale);
534 | widthScaled = 1 / textureScale;
535 | }
536 |
537 | return [widthScaled, heightScaled];
538 | }
539 |
540 | function allocateProjectionData(geometry, instancesCount) {
541 | geometry.setAttribute(`savedModelMatrix0`, new THREE.InstancedBufferAttribute(new Float32Array(instancesCount * 4), 4));
542 | geometry.setAttribute(`savedModelMatrix1`, new THREE.InstancedBufferAttribute(new Float32Array(instancesCount * 4), 4));
543 | geometry.setAttribute(`savedModelMatrix2`, new THREE.InstancedBufferAttribute(new Float32Array(instancesCount * 4), 4));
544 | geometry.setAttribute(`savedModelMatrix3`, new THREE.InstancedBufferAttribute(new Float32Array(instancesCount * 4), 4));
545 | }
546 |
547 | export { allocateProjectionData, ProjectedMaterial as default };
548 |
--------------------------------------------------------------------------------
/examples/3d-model.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | 3D Model example - three-projected-material
7 |
8 |
9 |
10 |
11 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
31 |
32 |
100 |
101 |
102 |
--------------------------------------------------------------------------------
/examples/basic.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Basic example - three-projected-material
7 |
8 |
9 |
10 |
11 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
31 |
32 |
92 |
93 |
94 |
--------------------------------------------------------------------------------
/examples/css/style.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | padding: 0;
4 | }
5 |
6 | .source-fab {
7 | display: block;
8 | position: fixed;
9 | bottom: 1.5rem;
10 | right: 1.5rem;
11 | padding: 0.75rem;
12 | border-radius: 50%;
13 | background-color: #f1f1f1;
14 | z-index: 999;
15 | box-shadow: 0 3px 5px rgba(0, 0, 0, 0.2);
16 | cursor: pointer;
17 | }
18 | .source-fab img {
19 | display: block;
20 | width: 20px;
21 | }
22 |
23 | canvas {
24 | outline: none;
25 | display: block;
26 | }
27 |
28 | .title {
29 | position: fixed;
30 | top: 0;
31 | left: 50%;
32 | transform: translateX(-50%);
33 | max-width: 90vw;
34 | font-family: monospace;
35 | text-align: center;
36 | }
37 |
38 | .title a {
39 | color: #05f;
40 | }
41 |
42 | .title.white {
43 | color: white;
44 | }
45 | .title.white a {
46 | color: #0f0;
47 | }
48 |
--------------------------------------------------------------------------------
/examples/envmap.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Envmap example - three-projected-material
7 |
8 |
9 |
10 |
11 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
31 |
32 |
94 |
95 |
96 |
--------------------------------------------------------------------------------
/examples/images/bigbucksbunny.mp4:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wb-ts/three-js-examples/b59e795c353df9c24b635e2718add0106d415f3a/examples/images/bigbucksbunny.mp4
--------------------------------------------------------------------------------
/examples/images/black-spot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wb-ts/three-js-examples/b59e795c353df9c24b635e2718add0106d415f3a/examples/images/black-spot.png
--------------------------------------------------------------------------------
/examples/images/charles-unsplash.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wb-ts/three-js-examples/b59e795c353df9c24b635e2718add0106d415f3a/examples/images/charles-unsplash.jpg
--------------------------------------------------------------------------------
/examples/images/kandao3_blurred.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wb-ts/three-js-examples/b59e795c353df9c24b635e2718add0106d415f3a/examples/images/kandao3_blurred.jpg
--------------------------------------------------------------------------------
/examples/images/lukasz-szmigiel-unsplash.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wb-ts/three-js-examples/b59e795c353df9c24b635e2718add0106d415f3a/examples/images/lukasz-szmigiel-unsplash.jpg
--------------------------------------------------------------------------------
/examples/images/source.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/examples/images/three-projected-material-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wb-ts/three-js-examples/b59e795c353df9c24b635e2718add0106d415f3a/examples/images/three-projected-material-1.png
--------------------------------------------------------------------------------
/examples/images/three-projected-material-2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wb-ts/three-js-examples/b59e795c353df9c24b635e2718add0106d415f3a/examples/images/three-projected-material-2.png
--------------------------------------------------------------------------------
/examples/images/three-projected-material-3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wb-ts/three-js-examples/b59e795c353df9c24b635e2718add0106d415f3a/examples/images/three-projected-material-3.png
--------------------------------------------------------------------------------
/examples/images/three-projected-material-4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wb-ts/three-js-examples/b59e795c353df9c24b635e2718add0106d415f3a/examples/images/three-projected-material-4.png
--------------------------------------------------------------------------------
/examples/images/three-projected-material-5.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wb-ts/three-js-examples/b59e795c353df9c24b635e2718add0106d415f3a/examples/images/three-projected-material-5.png
--------------------------------------------------------------------------------
/examples/images/three-projected-material-6.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wb-ts/three-js-examples/b59e795c353df9c24b635e2718add0106d415f3a/examples/images/three-projected-material-6.png
--------------------------------------------------------------------------------
/examples/images/uv.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wb-ts/three-js-examples/b59e795c353df9c24b635e2718add0106d415f3a/examples/images/uv.jpg
--------------------------------------------------------------------------------
/examples/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | three-projected-material
7 |
11 |
12 |
13 |
14 |
132 |
133 |
134 |
135 |
136 |
137 |
144 |
145 |
194 |
195 |
325 |
326 |
327 |