three-video-projection 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 屈航
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
13
+ all 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
21
+ THE SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,143 @@
1
+ ⭐ 开源不易,点个Star支持下吧 ⭐
2
+
3
+ ---
4
+
5
+ [英文](README_En.md)
6
+
7
+ ---
8
+
9
+ # three-video-projector
10
+
11
+ [![NPM Package][npm]][npm-url]
12
+
13
+ 基于 `three.js` 的视频投影工具。
14
+
15
+ > 该工具将 `THREE.VideoTexture` 从一个投影相机投影到场景中的目标模型上,支持深度遮挡剔除、边缘羽化、强度与透明度控制等。
16
+
17
+ ---
18
+
19
+ # 示例
20
+
21
+ - [视频融合](https://hh-hang.github.io/three-video-projection/)
22
+
23
+ - [电影院](https://hh-hang.github.io/three-video-projection/cinema.html)
24
+
25
+ - ![视频融合](https://github.com/hh-hang/three-video-projection/blob/master/example/public/imgs/2.gif)
26
+
27
+ - ![电影院](https://github.com/hh-hang/three-video-projection/blob/master/example/public/imgs/1.gif)
28
+
29
+ ---
30
+
31
+ ## 特性
32
+
33
+ - 将视频纹理投影到场景中指定的模型(Mesh)。
34
+ - 使用深度渲染目标进行遮挡剔除(保证投影不会穿透)。
35
+ - 边缘羽化以获得柔和的投影边缘。
36
+ - 可调整投影相机位置与姿态(方位角/俯仰/滚转)。
37
+ - 监控视频-设置合适参数能做到与视频融合。
38
+
39
+ ---
40
+
41
+ ## 安装
42
+
43
+ ```bash
44
+ npm install three-video-projection
45
+ ```
46
+
47
+ ---
48
+
49
+ ## 快速开始
50
+
51
+ ```ts
52
+ import * as THREE from "three";
53
+ import { createVideoProjector } from "./path/to/projector";
54
+
55
+ // 创建video元素并生成 VideoTexture
56
+ const video = document.createElement("video");
57
+ video.src = "path/to/video.mp4";
58
+ video.loop = true;
59
+ video.muted = true;
60
+ video.playsInline = true;
61
+ await video.play();
62
+ const videoTexture = new THREE.VideoTexture(video);
63
+
64
+ // 注:可以使用视频流,只要保证最后成功创建videoTexture
65
+
66
+ // 创建投影器
67
+ const projector = await createVideoProjector({
68
+ scene, // three 场景
69
+ renderer, // three 渲染器
70
+ videoTexture, // 视频纹理
71
+ projCamPosition: [2, 2, 2], // 投影相机位置
72
+ projCamParams: { fov: 30, aspect: 1, near: 0.5, far: 50 }, // 投影相机参数
73
+ orientationParams: { azimuthDeg: 180, elevationDeg: -10, rollDeg: 0 }, // 方位角/俯仰/滚转
74
+ intensity: 1.0, // 投影颜色强度
75
+ opacity: 1.0, // 投影透明度
76
+ projBias: 0.0001, // 深度偏移
77
+ edgeFeather: 0.05, // 边缘羽化程度
78
+ isShowHelper: true, // 是否显示相机辅助器
79
+ });
80
+
81
+ // 将需要被投影的 mesh 加入
82
+ projector.addTargetMesh(myMesh1);
83
+
84
+ // 渲染循环
85
+ function animate() {
86
+ // ... 更新场景、控制器
87
+ projector.update();
88
+ }
89
+ animate();
90
+
91
+ // 销毁时
92
+ projector.dispose();
93
+ ```
94
+
95
+ ---
96
+
97
+ ## API
98
+
99
+ ### createVideoProjector(opts: ProjectorToolOptions): Promise<ProjectorTool>
100
+
101
+ #### ProjectorToolOptions
102
+
103
+ - `scene: THREE.Scene` — three 场景(必需)。
104
+ - `renderer: THREE.WebGLRenderer` — three 渲染器(必需)。
105
+ - `videoTexture: THREE.VideoTexture` — 用于投影的视频纹理(必需)。
106
+ - `projCamPosition?: [number, number, number]` — 投影相机在世界空间的位置。默认 `[0,0,0]`。
107
+ - `projCamParams?: { fov?: number; aspect?: number; near?: number; far?: number }` — 投影相机参数。默认 `{ fov: 30, aspect: 1, near: 0.5, far: 50 }`。
108
+ - `orientationParams?: { azimuthDeg?: number; elevationDeg?: number; rollDeg?: number }` — 方位角/俯仰/滚转(度)。默认均为 `0`。
109
+ - `depthSize?: number` — 深度渲染目标分辨率(宽/高)。默认 `1024`。
110
+ - `intensity?: number` — 投影颜色强度,默认 `1.0`。
111
+ - `opacity?: number` — 全局透明度,默认 `1.0`。
112
+ - `projBias?: number` — 深度偏移,默认 `0.0001`。
113
+ - `edgeFeather?: number` — 边缘羽化宽度,默认 `0.05`。
114
+ - `isShowHelper?: boolean` — 是否显示 `CameraHelper` 来可视化投影相机,默认 `true`。
115
+
116
+ ---
117
+
118
+ #### ProjectorTool (返回对象)
119
+
120
+ 方法:
121
+
122
+ - `addTargetMesh(mesh: THREE.Mesh): void` — 将目标网格加入投影列表。会在场景中创建一个 overlay(投影用)和 depth proxy(用于深度渲染)。
123
+ - `removeTargetMesh(mesh: THREE.Mesh): void` — 从投影列表移除指定 mesh,并清理对应资源。
124
+ - `update(): void` — 每帧调用以更新深度渲染目标、投影矩阵,并同步 overlay 的矩阵。
125
+ - `dispose(): void` — 销毁内部资源并从场景中移除创建的对象。
126
+ - `updateAzimuthDeg(deg: number): void` — 设置方位角(度)并应用到投影相机。
127
+ - `updateElevationDeg(deg: number): void` — 设置俯仰角(度)。
128
+ - `updateRollDeg(deg: number): void` — 设置滚转角(度)。
129
+ - `updateOpacity(opacity: number): void` — 更新投影透明度(0~1)。
130
+
131
+ 属性:
132
+
133
+ - `uniforms` — 暴露给外部的着色器 uniform 对象(包含 `projectorMap`、`projectorDepthMap`、`projectorMatrix`、`intensity`、`projBias`、`edgeFeather`、`opacity` 等)。
134
+ - `overlays: THREE.Mesh[]` — 内部创建的 overlay 列表(投影用透明网格)。
135
+ - `targetMeshes: THREE.Mesh[]` — 当前被投影的目标网格列表。
136
+ - `projCam: THREE.PerspectiveCamera` — 用于投影的相机。
137
+ - `camHelper: THREE.CameraHelper | null` — 可选的相机辅助器实例。
138
+ - `orientationParams` — 当前的方位/俯仰/滚转角(度)。
139
+
140
+ ---
141
+
142
+ [npm]: https://img.shields.io/npm/v/
143
+ [npm-url]: https://www.npmjs.com/package/
@@ -0,0 +1,48 @@
1
+ import * as THREE from 'three';
2
+
3
+ type ProjectorToolOptions = {
4
+ scene: THREE.Scene;
5
+ renderer: THREE.WebGLRenderer;
6
+ videoTexture: THREE.VideoTexture;
7
+ projCamPosition?: [number, number, number];
8
+ projCamParams?: {
9
+ fov?: number;
10
+ aspect?: number;
11
+ near?: number;
12
+ far?: number;
13
+ };
14
+ orientationParams?: {
15
+ azimuthDeg?: number;
16
+ elevationDeg?: number;
17
+ rollDeg?: number;
18
+ };
19
+ depthSize?: number;
20
+ intensity?: number;
21
+ opacity?: number;
22
+ projBias?: number;
23
+ edgeFeather?: number;
24
+ isShowHelper?: boolean;
25
+ };
26
+ type ProjectorTool = {
27
+ addTargetMesh: (mesh: THREE.Mesh) => void;
28
+ removeTargetMesh: (mesh: THREE.Mesh) => void;
29
+ update: () => void;
30
+ dispose: () => void;
31
+ updateAzimuthDeg: (deg: number) => void;
32
+ updateElevationDeg: (deg: number) => void;
33
+ updateRollDeg: (deg: number) => void;
34
+ updateOpacity: (opacity: number) => void;
35
+ uniforms: any;
36
+ overlays: THREE.Mesh[];
37
+ targetMeshes: THREE.Mesh[];
38
+ projCam: THREE.PerspectiveCamera;
39
+ camHelper: THREE.CameraHelper | null;
40
+ orientationParams: {
41
+ azimuthDeg: number;
42
+ elevationDeg: number;
43
+ rollDeg: number;
44
+ };
45
+ };
46
+ declare function createVideoProjector(opts: ProjectorToolOptions): Promise<ProjectorTool>;
47
+
48
+ export { type ProjectorTool, type ProjectorToolOptions, createVideoProjector };
@@ -0,0 +1,48 @@
1
+ import * as THREE from 'three';
2
+
3
+ type ProjectorToolOptions = {
4
+ scene: THREE.Scene;
5
+ renderer: THREE.WebGLRenderer;
6
+ videoTexture: THREE.VideoTexture;
7
+ projCamPosition?: [number, number, number];
8
+ projCamParams?: {
9
+ fov?: number;
10
+ aspect?: number;
11
+ near?: number;
12
+ far?: number;
13
+ };
14
+ orientationParams?: {
15
+ azimuthDeg?: number;
16
+ elevationDeg?: number;
17
+ rollDeg?: number;
18
+ };
19
+ depthSize?: number;
20
+ intensity?: number;
21
+ opacity?: number;
22
+ projBias?: number;
23
+ edgeFeather?: number;
24
+ isShowHelper?: boolean;
25
+ };
26
+ type ProjectorTool = {
27
+ addTargetMesh: (mesh: THREE.Mesh) => void;
28
+ removeTargetMesh: (mesh: THREE.Mesh) => void;
29
+ update: () => void;
30
+ dispose: () => void;
31
+ updateAzimuthDeg: (deg: number) => void;
32
+ updateElevationDeg: (deg: number) => void;
33
+ updateRollDeg: (deg: number) => void;
34
+ updateOpacity: (opacity: number) => void;
35
+ uniforms: any;
36
+ overlays: THREE.Mesh[];
37
+ targetMeshes: THREE.Mesh[];
38
+ projCam: THREE.PerspectiveCamera;
39
+ camHelper: THREE.CameraHelper | null;
40
+ orientationParams: {
41
+ azimuthDeg: number;
42
+ elevationDeg: number;
43
+ rollDeg: number;
44
+ };
45
+ };
46
+ declare function createVideoProjector(opts: ProjectorToolOptions): Promise<ProjectorTool>;
47
+
48
+ export { type ProjectorTool, type ProjectorToolOptions, createVideoProjector };
package/dist/index.js ADDED
@@ -0,0 +1,305 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/index.ts
31
+ var index_exports = {};
32
+ __export(index_exports, {
33
+ createVideoProjector: () => createVideoProjector
34
+ });
35
+ module.exports = __toCommonJS(index_exports);
36
+
37
+ // src/three-video-projection.ts
38
+ var THREE = __toESM(require("three"));
39
+ async function createVideoProjector(opts) {
40
+ var _a, _b, _c, _d, _e, _f, _g;
41
+ const {
42
+ scene,
43
+ renderer,
44
+ videoTexture,
45
+ projCamPosition = [0, 0, 0],
46
+ projCamParams = { fov: 30, aspect: 1, near: 0.5, far: 50 },
47
+ orientationParams = { azimuthDeg: 0, elevationDeg: 0, rollDeg: 0 },
48
+ depthSize = 1024,
49
+ intensity = 1,
50
+ opacity = 1,
51
+ projBias = 1e-4,
52
+ edgeFeather = 0.05,
53
+ isShowHelper = true
54
+ } = opts;
55
+ let orientParams = {
56
+ azimuthDeg: (_a = orientationParams.azimuthDeg) != null ? _a : 0,
57
+ elevationDeg: (_b = orientationParams.elevationDeg) != null ? _b : 0,
58
+ rollDeg: (_c = orientationParams.rollDeg) != null ? _c : 0
59
+ };
60
+ let projCam;
61
+ let camHelper = null;
62
+ projCam = new THREE.PerspectiveCamera(
63
+ (_d = projCamParams.fov) != null ? _d : 30,
64
+ (_e = projCamParams.aspect) != null ? _e : 1,
65
+ (_f = projCamParams.near) != null ? _f : 0.5,
66
+ (_g = projCamParams.far) != null ? _g : 50
67
+ );
68
+ projCam.position.set(
69
+ projCamPosition[0],
70
+ projCamPosition[1],
71
+ projCamPosition[2]
72
+ );
73
+ projCam.lookAt(0, 0, 0);
74
+ scene.add(projCam);
75
+ applyOrientationFromAngles();
76
+ camHelper = new THREE.CameraHelper(projCam);
77
+ camHelper.name = "camHelper";
78
+ camHelper.visible = isShowHelper;
79
+ scene.add(camHelper);
80
+ videoTexture.minFilter = THREE.LinearFilter;
81
+ videoTexture.generateMipmaps = false;
82
+ const projectorUniforms = {
83
+ projectorMap: { value: videoTexture },
84
+ projectorMatrix: { value: new THREE.Matrix4() },
85
+ intensity: { value: intensity },
86
+ projectorDepthMap: { value: null },
87
+ projBias: { value: projBias },
88
+ edgeFeather: { value: edgeFeather },
89
+ opacity: { value: opacity }
90
+ };
91
+ const vertexShader = `
92
+ varying vec3 vWorldPos;
93
+ varying vec3 vWorldNormal;
94
+ void main() {
95
+ vec4 worldPos = modelMatrix * vec4(position, 1.0);
96
+ vWorldPos = worldPos.xyz;
97
+ vWorldNormal = normalize(mat3(modelMatrix) * normal);
98
+ gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
99
+ }
100
+ `;
101
+ const fragmentShader = `
102
+ uniform sampler2D projectorMap;
103
+ uniform sampler2D projectorDepthMap;
104
+ uniform mat4 projectorMatrix;
105
+ uniform float intensity;
106
+ uniform float projBias;
107
+ uniform float edgeFeather;
108
+ uniform float opacity;
109
+ varying vec3 vWorldPos;
110
+ varying vec3 vWorldNormal;
111
+
112
+ void main() {
113
+ vec4 projPos = projectorMatrix * vec4(vWorldPos, 1.0);
114
+ if (projPos.w <= 0.0) discard;
115
+ vec2 uv = projPos.xy / projPos.w * 0.5 + 0.5;
116
+ if (uv.x < 0.0 || uv.x > 1.0 || uv.y < 0.0 || uv.y > 1.0) discard;
117
+ vec4 color = texture(projectorMap, uv);
118
+
119
+ // \u906E\u6321\u5254\u9664
120
+ float projNDCz = projPos.z / projPos.w;
121
+ float projDepth01 = projNDCz * 0.5 + 0.5;
122
+ float sceneDepth01 = texture(projectorDepthMap, uv).x;
123
+ if (projDepth01 > sceneDepth01 + projBias) {
124
+ discard;
125
+ }
126
+
127
+ // \u8FB9\u7F18\u7FBD\u5316
128
+ vec2 adjUV = uv;
129
+ float minDist = min(min(adjUV.x, 1.0 - adjUV.x), min(adjUV.y, 1.0 - adjUV.y));
130
+ float edgeFactor = 1.0;
131
+ if (edgeFeather > 0.0) {
132
+ edgeFactor = smoothstep(0.0, edgeFeather, minDist);
133
+ }
134
+ float effectiveAlpha = color.a * edgeFactor;
135
+
136
+ // \u8F93\u51FA
137
+ vec3 outRGB = color.rgb * intensity * edgeFactor * opacity;
138
+ float outA = effectiveAlpha * opacity;
139
+ gl_FragColor = vec4(outRGB, outA);
140
+ }
141
+ `;
142
+ const projectorMat = new THREE.ShaderMaterial({
143
+ uniforms: projectorUniforms,
144
+ vertexShader,
145
+ fragmentShader,
146
+ transparent: true,
147
+ depthWrite: false,
148
+ depthTest: true,
149
+ side: THREE.FrontSide,
150
+ polygonOffset: true,
151
+ polygonOffsetFactor: -1,
152
+ polygonOffsetUnits: -4,
153
+ alphaTest: 0.02
154
+ });
155
+ const projectorDepthRT = new THREE.WebGLRenderTarget(depthSize, depthSize, {
156
+ minFilter: THREE.NearestFilter,
157
+ magFilter: THREE.NearestFilter,
158
+ stencilBuffer: false,
159
+ depthBuffer: true
160
+ });
161
+ projectorDepthRT.depthTexture = new THREE.DepthTexture(
162
+ depthSize,
163
+ depthSize,
164
+ THREE.UnsignedShortType
165
+ );
166
+ const depthScene = new THREE.Scene();
167
+ const depthMaterial = new THREE.MeshDepthMaterial();
168
+ depthMaterial.depthPacking = THREE.RGBADepthPacking;
169
+ depthMaterial.side = THREE.FrontSide;
170
+ const overlays = [];
171
+ const targetMeshes = [];
172
+ const depthProxies = [];
173
+ function makeProjectorOverlayAndProxy(mesh) {
174
+ const overlay = new THREE.Mesh(mesh.geometry, projectorMat);
175
+ overlay.matrixAutoUpdate = false;
176
+ overlay.renderOrder = (mesh.renderOrder || 0) + 1;
177
+ mesh.updateMatrixWorld(true);
178
+ overlay.matrix.copy(mesh.matrixWorld);
179
+ scene.add(overlay);
180
+ const proxy = new THREE.Mesh(mesh.geometry, depthMaterial);
181
+ proxy.matrixAutoUpdate = false;
182
+ depthScene.add(proxy);
183
+ overlays.push(overlay);
184
+ depthProxies.push(proxy);
185
+ return { overlay, proxy };
186
+ }
187
+ function addTargetMesh(mesh) {
188
+ if (targetMeshes.indexOf(mesh) !== -1) return;
189
+ mesh.castShadow = true;
190
+ mesh.receiveShadow = true;
191
+ targetMeshes.push(mesh);
192
+ makeProjectorOverlayAndProxy(mesh);
193
+ }
194
+ function removeTargetMesh(mesh) {
195
+ const idx = targetMeshes.indexOf(mesh);
196
+ if (idx === -1) return;
197
+ targetMeshes.splice(idx, 1);
198
+ const ov = overlays.splice(idx, 1)[0];
199
+ if (ov) scene.remove(ov);
200
+ const proxy = depthProxies.splice(idx, 1)[0];
201
+ if (proxy) depthScene.remove(proxy);
202
+ }
203
+ function update() {
204
+ for (let i = 0; i < targetMeshes.length; i++) {
205
+ const src = targetMeshes[i];
206
+ const proxy = depthProxies[i];
207
+ src.updateMatrixWorld(true);
208
+ proxy.matrix.copy(src.matrixWorld);
209
+ }
210
+ renderer.setRenderTarget(projectorDepthRT);
211
+ renderer.clear();
212
+ renderer.render(depthScene, projCam);
213
+ renderer.setRenderTarget(null);
214
+ projectorUniforms.projectorDepthMap.value = projectorDepthRT.depthTexture;
215
+ const projectorMatrix = new THREE.Matrix4();
216
+ projectorMatrix.multiplyMatrices(
217
+ projCam.projectionMatrix,
218
+ projCam.matrixWorldInverse
219
+ );
220
+ projectorUniforms.projectorMatrix.value.copy(projectorMatrix);
221
+ for (let i = 0; i < targetMeshes.length; i++) {
222
+ const src = targetMeshes[i];
223
+ const overlay = overlays[i];
224
+ src.updateMatrixWorld(true);
225
+ overlay.matrix.copy(src.matrixWorld);
226
+ }
227
+ }
228
+ function dispose() {
229
+ for (let ov of overlays) scene.remove(ov);
230
+ for (let p of depthProxies) depthScene.remove(p);
231
+ overlays.length = 0;
232
+ depthProxies.length = 0;
233
+ targetMeshes.length = 0;
234
+ projectorMat.dispose();
235
+ depthMaterial.dispose();
236
+ try {
237
+ projectorDepthRT.dispose();
238
+ } catch (e) {
239
+ }
240
+ try {
241
+ videoTexture.dispose();
242
+ } catch (e) {
243
+ }
244
+ if (camHelper) {
245
+ try {
246
+ scene.remove(camHelper);
247
+ } catch (e) {
248
+ }
249
+ camHelper = null;
250
+ }
251
+ }
252
+ function updateAzimuthDeg(deg) {
253
+ orientParams.azimuthDeg = deg;
254
+ applyOrientationFromAngles();
255
+ }
256
+ function updateElevationDeg(deg) {
257
+ orientParams.elevationDeg = deg;
258
+ applyOrientationFromAngles();
259
+ }
260
+ function updateRollDeg(deg) {
261
+ orientParams.rollDeg = deg;
262
+ applyOrientationFromAngles();
263
+ }
264
+ function applyOrientationFromAngles() {
265
+ const az = THREE.MathUtils.degToRad(orientParams.azimuthDeg);
266
+ const el = THREE.MathUtils.degToRad(orientParams.elevationDeg);
267
+ const dir = new THREE.Vector3(
268
+ Math.cos(el) * Math.cos(az),
269
+ Math.sin(el),
270
+ Math.cos(el) * Math.sin(az)
271
+ ).normalize();
272
+ const lookTarget = new THREE.Vector3().copy(projCam.position).add(dir);
273
+ projCam.up.set(0, 1, 0);
274
+ projCam.lookAt(lookTarget);
275
+ projCam.updateMatrixWorld(true);
276
+ const rollRad = THREE.MathUtils.degToRad(orientParams.rollDeg);
277
+ projCam.rotateOnAxis(new THREE.Vector3(0, 0, 1), rollRad);
278
+ projCam.updateMatrixWorld(true);
279
+ if (camHelper) camHelper.update();
280
+ }
281
+ function updateOpacity(v) {
282
+ const clamped = Math.max(0, Math.min(1, v));
283
+ projectorUniforms.opacity.value = clamped;
284
+ }
285
+ return {
286
+ addTargetMesh,
287
+ removeTargetMesh,
288
+ update,
289
+ dispose,
290
+ updateAzimuthDeg,
291
+ updateElevationDeg,
292
+ updateRollDeg,
293
+ updateOpacity,
294
+ uniforms: projectorUniforms,
295
+ overlays,
296
+ targetMeshes,
297
+ projCam,
298
+ camHelper,
299
+ orientationParams: orientParams
300
+ };
301
+ }
302
+ // Annotate the CommonJS export names for ESM import in node:
303
+ 0 && (module.exports = {
304
+ createVideoProjector
305
+ });
package/dist/index.mjs ADDED
@@ -0,0 +1,268 @@
1
+ // src/three-video-projection.ts
2
+ import * as THREE from "three";
3
+ async function createVideoProjector(opts) {
4
+ var _a, _b, _c, _d, _e, _f, _g;
5
+ const {
6
+ scene,
7
+ renderer,
8
+ videoTexture,
9
+ projCamPosition = [0, 0, 0],
10
+ projCamParams = { fov: 30, aspect: 1, near: 0.5, far: 50 },
11
+ orientationParams = { azimuthDeg: 0, elevationDeg: 0, rollDeg: 0 },
12
+ depthSize = 1024,
13
+ intensity = 1,
14
+ opacity = 1,
15
+ projBias = 1e-4,
16
+ edgeFeather = 0.05,
17
+ isShowHelper = true
18
+ } = opts;
19
+ let orientParams = {
20
+ azimuthDeg: (_a = orientationParams.azimuthDeg) != null ? _a : 0,
21
+ elevationDeg: (_b = orientationParams.elevationDeg) != null ? _b : 0,
22
+ rollDeg: (_c = orientationParams.rollDeg) != null ? _c : 0
23
+ };
24
+ let projCam;
25
+ let camHelper = null;
26
+ projCam = new THREE.PerspectiveCamera(
27
+ (_d = projCamParams.fov) != null ? _d : 30,
28
+ (_e = projCamParams.aspect) != null ? _e : 1,
29
+ (_f = projCamParams.near) != null ? _f : 0.5,
30
+ (_g = projCamParams.far) != null ? _g : 50
31
+ );
32
+ projCam.position.set(
33
+ projCamPosition[0],
34
+ projCamPosition[1],
35
+ projCamPosition[2]
36
+ );
37
+ projCam.lookAt(0, 0, 0);
38
+ scene.add(projCam);
39
+ applyOrientationFromAngles();
40
+ camHelper = new THREE.CameraHelper(projCam);
41
+ camHelper.name = "camHelper";
42
+ camHelper.visible = isShowHelper;
43
+ scene.add(camHelper);
44
+ videoTexture.minFilter = THREE.LinearFilter;
45
+ videoTexture.generateMipmaps = false;
46
+ const projectorUniforms = {
47
+ projectorMap: { value: videoTexture },
48
+ projectorMatrix: { value: new THREE.Matrix4() },
49
+ intensity: { value: intensity },
50
+ projectorDepthMap: { value: null },
51
+ projBias: { value: projBias },
52
+ edgeFeather: { value: edgeFeather },
53
+ opacity: { value: opacity }
54
+ };
55
+ const vertexShader = `
56
+ varying vec3 vWorldPos;
57
+ varying vec3 vWorldNormal;
58
+ void main() {
59
+ vec4 worldPos = modelMatrix * vec4(position, 1.0);
60
+ vWorldPos = worldPos.xyz;
61
+ vWorldNormal = normalize(mat3(modelMatrix) * normal);
62
+ gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
63
+ }
64
+ `;
65
+ const fragmentShader = `
66
+ uniform sampler2D projectorMap;
67
+ uniform sampler2D projectorDepthMap;
68
+ uniform mat4 projectorMatrix;
69
+ uniform float intensity;
70
+ uniform float projBias;
71
+ uniform float edgeFeather;
72
+ uniform float opacity;
73
+ varying vec3 vWorldPos;
74
+ varying vec3 vWorldNormal;
75
+
76
+ void main() {
77
+ vec4 projPos = projectorMatrix * vec4(vWorldPos, 1.0);
78
+ if (projPos.w <= 0.0) discard;
79
+ vec2 uv = projPos.xy / projPos.w * 0.5 + 0.5;
80
+ if (uv.x < 0.0 || uv.x > 1.0 || uv.y < 0.0 || uv.y > 1.0) discard;
81
+ vec4 color = texture(projectorMap, uv);
82
+
83
+ // \u906E\u6321\u5254\u9664
84
+ float projNDCz = projPos.z / projPos.w;
85
+ float projDepth01 = projNDCz * 0.5 + 0.5;
86
+ float sceneDepth01 = texture(projectorDepthMap, uv).x;
87
+ if (projDepth01 > sceneDepth01 + projBias) {
88
+ discard;
89
+ }
90
+
91
+ // \u8FB9\u7F18\u7FBD\u5316
92
+ vec2 adjUV = uv;
93
+ float minDist = min(min(adjUV.x, 1.0 - adjUV.x), min(adjUV.y, 1.0 - adjUV.y));
94
+ float edgeFactor = 1.0;
95
+ if (edgeFeather > 0.0) {
96
+ edgeFactor = smoothstep(0.0, edgeFeather, minDist);
97
+ }
98
+ float effectiveAlpha = color.a * edgeFactor;
99
+
100
+ // \u8F93\u51FA
101
+ vec3 outRGB = color.rgb * intensity * edgeFactor * opacity;
102
+ float outA = effectiveAlpha * opacity;
103
+ gl_FragColor = vec4(outRGB, outA);
104
+ }
105
+ `;
106
+ const projectorMat = new THREE.ShaderMaterial({
107
+ uniforms: projectorUniforms,
108
+ vertexShader,
109
+ fragmentShader,
110
+ transparent: true,
111
+ depthWrite: false,
112
+ depthTest: true,
113
+ side: THREE.FrontSide,
114
+ polygonOffset: true,
115
+ polygonOffsetFactor: -1,
116
+ polygonOffsetUnits: -4,
117
+ alphaTest: 0.02
118
+ });
119
+ const projectorDepthRT = new THREE.WebGLRenderTarget(depthSize, depthSize, {
120
+ minFilter: THREE.NearestFilter,
121
+ magFilter: THREE.NearestFilter,
122
+ stencilBuffer: false,
123
+ depthBuffer: true
124
+ });
125
+ projectorDepthRT.depthTexture = new THREE.DepthTexture(
126
+ depthSize,
127
+ depthSize,
128
+ THREE.UnsignedShortType
129
+ );
130
+ const depthScene = new THREE.Scene();
131
+ const depthMaterial = new THREE.MeshDepthMaterial();
132
+ depthMaterial.depthPacking = THREE.RGBADepthPacking;
133
+ depthMaterial.side = THREE.FrontSide;
134
+ const overlays = [];
135
+ const targetMeshes = [];
136
+ const depthProxies = [];
137
+ function makeProjectorOverlayAndProxy(mesh) {
138
+ const overlay = new THREE.Mesh(mesh.geometry, projectorMat);
139
+ overlay.matrixAutoUpdate = false;
140
+ overlay.renderOrder = (mesh.renderOrder || 0) + 1;
141
+ mesh.updateMatrixWorld(true);
142
+ overlay.matrix.copy(mesh.matrixWorld);
143
+ scene.add(overlay);
144
+ const proxy = new THREE.Mesh(mesh.geometry, depthMaterial);
145
+ proxy.matrixAutoUpdate = false;
146
+ depthScene.add(proxy);
147
+ overlays.push(overlay);
148
+ depthProxies.push(proxy);
149
+ return { overlay, proxy };
150
+ }
151
+ function addTargetMesh(mesh) {
152
+ if (targetMeshes.indexOf(mesh) !== -1) return;
153
+ mesh.castShadow = true;
154
+ mesh.receiveShadow = true;
155
+ targetMeshes.push(mesh);
156
+ makeProjectorOverlayAndProxy(mesh);
157
+ }
158
+ function removeTargetMesh(mesh) {
159
+ const idx = targetMeshes.indexOf(mesh);
160
+ if (idx === -1) return;
161
+ targetMeshes.splice(idx, 1);
162
+ const ov = overlays.splice(idx, 1)[0];
163
+ if (ov) scene.remove(ov);
164
+ const proxy = depthProxies.splice(idx, 1)[0];
165
+ if (proxy) depthScene.remove(proxy);
166
+ }
167
+ function update() {
168
+ for (let i = 0; i < targetMeshes.length; i++) {
169
+ const src = targetMeshes[i];
170
+ const proxy = depthProxies[i];
171
+ src.updateMatrixWorld(true);
172
+ proxy.matrix.copy(src.matrixWorld);
173
+ }
174
+ renderer.setRenderTarget(projectorDepthRT);
175
+ renderer.clear();
176
+ renderer.render(depthScene, projCam);
177
+ renderer.setRenderTarget(null);
178
+ projectorUniforms.projectorDepthMap.value = projectorDepthRT.depthTexture;
179
+ const projectorMatrix = new THREE.Matrix4();
180
+ projectorMatrix.multiplyMatrices(
181
+ projCam.projectionMatrix,
182
+ projCam.matrixWorldInverse
183
+ );
184
+ projectorUniforms.projectorMatrix.value.copy(projectorMatrix);
185
+ for (let i = 0; i < targetMeshes.length; i++) {
186
+ const src = targetMeshes[i];
187
+ const overlay = overlays[i];
188
+ src.updateMatrixWorld(true);
189
+ overlay.matrix.copy(src.matrixWorld);
190
+ }
191
+ }
192
+ function dispose() {
193
+ for (let ov of overlays) scene.remove(ov);
194
+ for (let p of depthProxies) depthScene.remove(p);
195
+ overlays.length = 0;
196
+ depthProxies.length = 0;
197
+ targetMeshes.length = 0;
198
+ projectorMat.dispose();
199
+ depthMaterial.dispose();
200
+ try {
201
+ projectorDepthRT.dispose();
202
+ } catch (e) {
203
+ }
204
+ try {
205
+ videoTexture.dispose();
206
+ } catch (e) {
207
+ }
208
+ if (camHelper) {
209
+ try {
210
+ scene.remove(camHelper);
211
+ } catch (e) {
212
+ }
213
+ camHelper = null;
214
+ }
215
+ }
216
+ function updateAzimuthDeg(deg) {
217
+ orientParams.azimuthDeg = deg;
218
+ applyOrientationFromAngles();
219
+ }
220
+ function updateElevationDeg(deg) {
221
+ orientParams.elevationDeg = deg;
222
+ applyOrientationFromAngles();
223
+ }
224
+ function updateRollDeg(deg) {
225
+ orientParams.rollDeg = deg;
226
+ applyOrientationFromAngles();
227
+ }
228
+ function applyOrientationFromAngles() {
229
+ const az = THREE.MathUtils.degToRad(orientParams.azimuthDeg);
230
+ const el = THREE.MathUtils.degToRad(orientParams.elevationDeg);
231
+ const dir = new THREE.Vector3(
232
+ Math.cos(el) * Math.cos(az),
233
+ Math.sin(el),
234
+ Math.cos(el) * Math.sin(az)
235
+ ).normalize();
236
+ const lookTarget = new THREE.Vector3().copy(projCam.position).add(dir);
237
+ projCam.up.set(0, 1, 0);
238
+ projCam.lookAt(lookTarget);
239
+ projCam.updateMatrixWorld(true);
240
+ const rollRad = THREE.MathUtils.degToRad(orientParams.rollDeg);
241
+ projCam.rotateOnAxis(new THREE.Vector3(0, 0, 1), rollRad);
242
+ projCam.updateMatrixWorld(true);
243
+ if (camHelper) camHelper.update();
244
+ }
245
+ function updateOpacity(v) {
246
+ const clamped = Math.max(0, Math.min(1, v));
247
+ projectorUniforms.opacity.value = clamped;
248
+ }
249
+ return {
250
+ addTargetMesh,
251
+ removeTargetMesh,
252
+ update,
253
+ dispose,
254
+ updateAzimuthDeg,
255
+ updateElevationDeg,
256
+ updateRollDeg,
257
+ updateOpacity,
258
+ uniforms: projectorUniforms,
259
+ overlays,
260
+ targetMeshes,
261
+ projCam,
262
+ camHelper,
263
+ orientationParams: orientParams
264
+ };
265
+ }
266
+ export {
267
+ createVideoProjector
268
+ };
package/package.json ADDED
@@ -0,0 +1,50 @@
1
+ {
2
+ "name": "three-video-projection",
3
+ "version": "0.0.1",
4
+ "description": "Projector utility for projecting video textures onto meshes (three.js)",
5
+ "main": "dist/index.cjs.js",
6
+ "module": "dist/index.esm.js",
7
+ "types": "dist/index.d.ts",
8
+ "files": [
9
+ "dist"
10
+ ],
11
+ "repository": {
12
+ "type": "git",
13
+ "url": "git+https://github.com/<your-org>/<repo>.git"
14
+ },
15
+ "keywords": [
16
+ "three",
17
+ "videoProjection",
18
+ "videoFusion"
19
+ ],
20
+ "author": "hh-hang",
21
+ "license": "MIT",
22
+ "scripts": {
23
+ "dev": "vite",
24
+ "build": "tsup src/index.ts --format cjs,esm --dts",
25
+ "prepare": "npm run build",
26
+ "build:cinema": "npm --prefix ./examples/cinema install && npm --prefix ./examples/cinema run build",
27
+ "build:monitor": "npm --prefix ./examples/monitor install && npm --prefix ./examples/monitor run build",
28
+ "collect": "node ./scripts/collect.js",
29
+ "build:examples": "npm run build:cinema && npm run build:monitor && npm run collect"
30
+ },
31
+ "peerDependencies": {
32
+ "three": "^0.182.0"
33
+ },
34
+ "devDependencies": {
35
+ "@types/node": "^25.0.10",
36
+ "@types/three": "^0.182.0",
37
+ "tsup": "^8.5.1",
38
+ "typescript": "^5.0.0"
39
+ },
40
+ "dependencies": {
41
+ "@vitejs/plugin-vue": "^6.0.3",
42
+ "hls.js": "^1.6.15",
43
+ "path": "^0.12.7",
44
+ "stats.js": "^0.17.0",
45
+ "three-player-controller": "^0.2.52",
46
+ "vite": "^7.3.1",
47
+ "vue": "^3.5.27",
48
+ "vue-router": "^4.6.4"
49
+ }
50
+ }