three-base 2.0.172 → 2.0.180
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/FPScontrol.d.ts +15 -24
- package/FPScontrol.js +63 -87
- package/FPScontrol.ts +64 -88
- package/Ground1/Ground1.d.ts +4 -2
- package/Ground1/Ground1.js +77 -57
- package/Ground1/Ground1.ts +77 -57
- package/Mirror.js +0 -4
- package/Mirror.ts +0 -4
- package/MktMesh/MktMesh.d.ts +3 -2
- package/MktMesh/MktMesh.js +27 -8
- package/MktMesh/MktMesh.ts +24 -8
- package/MktMesh/boxLine2.d.ts +42 -0
- package/MktMesh/boxLine2.js +185 -0
- package/MktMesh/boxLine2.ts +222 -0
- package/MktMesh/imagePlane.d.ts +60 -0
- package/MktMesh/imagePlane.js +158 -0
- package/MktMesh/imagePlane.ts +203 -0
- package/MktMesh/imageTileLoader.d.ts +19 -0
- package/MktMesh/imageTileLoader.js +55 -0
- package/MktMesh/imageTileLoader.ts +64 -0
- package/MktMesh/mktMappingShader.d.ts +6 -5
- package/MktMesh/mktMappingShader.js +26 -19
- package/MktMesh/mktMappingShader.ts +28 -19
- package/MktMesh/mktShader.d.ts +127 -58
- package/MktMesh/mktShader.js +192 -117
- package/MktMesh/mktShader.ts +251 -127
- package/MktMesh/modelSlot.d.ts +123 -0
- package/MktMesh/modelSlot.js +339 -0
- package/MktMesh/modelSlot.ts +383 -0
- package/MktMesh/panoSlotRoster.d.ts +2 -2
- package/MktMesh/panoSlotRoster.js +9 -7
- package/MktMesh/panoSlotRoster.ts +9 -7
- package/MktMesh/panoTileStitcher.d.ts +20 -13
- package/MktMesh/panoTileStitcher.js +97 -76
- package/MktMesh/panoTileStitcher.ts +119 -90
- package/PipeMesh/PipeJoint.d.ts +22 -0
- package/PipeMesh/PipeJoint.js +33 -0
- package/PipeMesh/PipeJoint.ts +38 -0
- package/PipeMesh/PipeMesh.d.ts +94 -22
- package/PipeMesh/PipeMesh.js +192 -53
- package/PipeMesh/PipeMesh.ts +223 -76
- package/PipeMesh/PipeStraight.d.ts +19 -0
- package/PipeMesh/PipeStraight.js +34 -0
- package/PipeMesh/PipeStraight.ts +45 -0
- package/PipeMesh/axisSnap.d.ts +52 -0
- package/PipeMesh/axisSnap.js +52 -0
- package/PipeMesh/axisSnap.ts +93 -0
- package/PipeMesh/directRoute.d.ts +85 -0
- package/PipeMesh/directRoute.js +126 -0
- package/PipeMesh/directRoute.ts +190 -0
- package/PipeMesh/index.d.ts +6 -0
- package/PipeMesh/index.js +6 -0
- package/PipeMesh/index.ts +7 -1
- package/PipeMesh/pipeMaterial.d.ts +16 -0
- package/PipeMesh/pipeMaterial.js +23 -0
- package/PipeMesh/pipeMaterial.ts +26 -0
- package/ThreeBase.d.ts +1 -39
- package/ThreeBase.js +38 -147
- package/ThreeBase.ts +1176 -1302
- package/instanced/InstancedBatch.d.ts +14 -1
- package/instanced/InstancedBatch.js +53 -3
- package/instanced/InstancedBatch.ts +62 -3
- package/instanced/InstancedChild.d.ts +24 -0
- package/instanced/InstancedChild.js +40 -0
- package/instanced/InstancedChild.ts +41 -0
- package/instanced/InstancedGroup.d.ts +48 -0
- package/instanced/InstancedGroup.js +45 -1
- package/instanced/InstancedGroup.ts +85 -0
- package/package.json +22 -7
- package/pass/debugDepthState.d.ts +16 -8
- package/pass/debugDepthState.js +4 -0
- package/pass/debugDepthState.ts +27 -9
- package/pass/glow_pass.js +11 -94
- package/pass/glow_pass.ts +11 -97
- package/pass/ssao_pass.d.ts +9 -23
- package/pass/ssao_pass.js +41 -128
- package/pass/ssao_pass.ts +36 -166
- package/pass/stroke_pass.js +3 -0
- package/pass/stroke_pass.ts +4 -0
- package/shaders/debugDepthCompareShader.d.ts +1 -1
- package/shaders/debugDepthCompareShader.js +1 -1
- package/shaders/debugDepthCompareShader.ts +1 -1
- package/shaders/debugDepthJudgeShader.d.ts +1 -1
- package/shaders/debugDepthJudgeShader.js +1 -1
- package/shaders/debugDepthJudgeShader.ts +1 -1
- package/PipeMesh/PathGeometry.d.ts +0 -44
- package/PipeMesh/PathGeometry.js +0 -307
- package/PipeMesh/PathGeometry.ts +0 -343
- package/PipeMesh/PathPoint.d.ts +0 -17
- package/PipeMesh/PathPoint.js +0 -31
- package/PipeMesh/PathPoint.ts +0 -36
- package/PipeMesh/PathPointList.d.ts +0 -31
- package/PipeMesh/PathPointList.js +0 -209
- package/PipeMesh/PathPointList.ts +0 -227
- package/PipeMesh/PathTubeGeometry.d.ts +0 -25
- package/PipeMesh/PathTubeGeometry.js +0 -130
- package/PipeMesh/PathTubeGeometry.ts +0 -146
- package/WaterFlow/FloatingDuck.d.ts +0 -88
- package/WaterFlow/FloatingDuck.js +0 -307
- package/WaterFlow/FloatingDuck.ts +0 -445
- package/WaterFlow/GPUFluidSimulation.d.ts +0 -56
- package/WaterFlow/GPUFluidSimulation.js +0 -785
- package/WaterFlow/GPUFluidSimulation.ts +0 -827
- package/WaterFlow/ReservoirSystem.d.ts +0 -106
- package/WaterFlow/ReservoirSystem.js +0 -621
- package/WaterFlow/ReservoirSystem.ts +0 -754
- package/WaterFlow/SPHField.d.ts +0 -35
- package/WaterFlow/SPHField.js +0 -204
- package/WaterFlow/SPHField.ts +0 -228
- package/WaterFlow/ScreenSpaceFluidPass.d.ts +0 -69
- package/WaterFlow/ScreenSpaceFluidPass.js +0 -736
- package/WaterFlow/ScreenSpaceFluidPass.ts +0 -789
- package/WaterFlow/ShallowWaterSurface.d.ts +0 -45
- package/WaterFlow/ShallowWaterSurface.js +0 -441
- package/WaterFlow/ShallowWaterSurface.ts +0 -510
- package/WaterFlow/WaterFlow.d.ts +0 -87
- package/WaterFlow/WaterFlow.js +0 -171
- package/WaterFlow/WaterFlow.ts +0 -219
- package/WaterFlow/types.d.ts +0 -161
- package/WaterFlow/types.js +0 -1
- package/WaterFlow/types.ts +0 -189
- package/WaterFlow/waterFlowPass.d.ts +0 -3
- package/WaterFlow/waterFlowPass.js +0 -20
- package/WaterFlow/waterFlowPass.ts +0 -40
- package/shaders/debugExcludedShader.d.ts +0 -15
- package/shaders/debugExcludedShader.js +0 -48
- package/shaders/debugExcludedShader.ts +0 -50
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* @Description: 选中设备的包围盒高亮框 —— 皇上旨 2026-10-22「选中设备的时候,设备还需要有一个高亮的框,
|
|
3
|
+
* 你参考旁边 archives2026 项目」⇒ 整搬那边的 `srcWeb/pages/LargeScreen/component3d/BoxLine2.ts`
|
|
4
|
+
* (12 条棱 fat-line:朝向相机那几棱**绿色实线**,被盒子自己挡住的那几棱**红色虚线**)。
|
|
5
|
+
*
|
|
6
|
+
* 为什么不直接用现成的 `EdgesGeometry + LineBasicMaterial`(也就是本仓旧版那圈细线):
|
|
7
|
+
* - 旧那圈是 `GL_LINES`,`linewidth` 在 WebGL/ANGLE 下是**死旋钮** ⇒ 永远 1 像素,深色模型上几乎看不见
|
|
8
|
+
* (仓内同族早已踩过:墙线/立方体边框 10-05、10-07 两旨都换成了 `LineSegments2 + LineMaterial`)。
|
|
9
|
+
* - fat-line 的粗细是**屏幕像素**(`worldUnits:false`)⇒ 远近一样粗,这正是「高亮框」要的效果。
|
|
10
|
+
*
|
|
11
|
+
* 遮挡那一刀怎么来的(照搬,⛔ 不是臣自己发明的判据):八顶点各自从相机朝该角发一条射线,打这份**内嵌代理盒**
|
|
12
|
+
* —— 打得着 = 这条视线被盒子自己截住了 = 那个角在背面 ⇒ 凡是含背角的棱改用红色虚线材质。
|
|
13
|
+
* 代理盒本身 `alphaTest: 1.1` ⇒ 一个像素都不出、也不写深度,只当射线靶子用。
|
|
14
|
+
*
|
|
15
|
+
* ⚠️ 相对参考实现改了五处,每处都只为了贴合本仓:
|
|
16
|
+
* ① 代理盒内缩量钳到「不小于原尺寸的 50%」(`_insetSize`):那边设备都是米级大物件,`v - 0.1` 恒正;
|
|
17
|
+
* 本仓的模型槽可能是薄一片(画作/展牌)或 0.3 m 的占位方块,`v - 0.1` 会出负数拿到一份翻面的几何。
|
|
18
|
+
* ② 整棵子树标 `userData.pickIgnored = true`:本仓编辑器拾取模型走 `intersectObjects(this.models, true)`
|
|
19
|
+
* (递归打进槽本体),一个实心代理盒挂在槽下面会把「框内的空地」也点成选中这只槽 —— 旧版那圈细线是靠
|
|
20
|
+
* `raycast = noop` 躲开的,这里换成标记,由 `_pickModel` 跳过(⛔ 类内部的遮挡射线不受影响,它打的是
|
|
21
|
+
* 自己那份 `boxMesh`,与本仓的拾取射线是两把尺)。
|
|
22
|
+
* ③ 加 `dispose()`:本仓模型槽删除/换模型要释放那 12 份线段几何与两份 LineMaterial,否则删一只漏一份显存。
|
|
23
|
+
* ④ 没搬那边那四颗 `setDepthTest` / `setLinewidth` / 两个 getter —— 在 archives2026 全仓 **0 处调用**
|
|
24
|
+
* (而且那两个私有字段的初值 `false`/`1` 与两份材质真正用的 `depthTest:true`/`linewidth:2` 本来就不一致,
|
|
25
|
+
* 只有从没被调过才没露馅)。真正生效的就是下面材质构造里那两个数,⛔ 不留没人拧的旋钮。
|
|
26
|
+
* ⑤ 12 条棱各自 `raycast = noop`:那边从不把线框交给射线,本仓的拾取射线会**递归**进槽子树打到它们,
|
|
27
|
+
* 而 `LineSegments2.raycast` 在 `worldUnits:false` 那一路要先读 `raycaster.camera`(编辑器那把没设)
|
|
28
|
+
* ⇒ 先 `console.error` 再撞 `camera.projectionMatrix` 抛异常。详见 `createLine` 里那句注释。
|
|
29
|
+
*/
|
|
30
|
+
import {
|
|
31
|
+
BoxGeometry,
|
|
32
|
+
BufferGeometry,
|
|
33
|
+
Camera,
|
|
34
|
+
DoubleSide,
|
|
35
|
+
Group,
|
|
36
|
+
Material,
|
|
37
|
+
Mesh,
|
|
38
|
+
MeshBasicMaterial,
|
|
39
|
+
NormalBufferAttributes,
|
|
40
|
+
Raycaster,
|
|
41
|
+
Scene,
|
|
42
|
+
Vector2,
|
|
43
|
+
Vector3,
|
|
44
|
+
WebGLRenderer,
|
|
45
|
+
} from "three";
|
|
46
|
+
import { LineMaterial } from "three/examples/jsm/lines/LineMaterial.js";
|
|
47
|
+
import { LineSegments2 } from "three/examples/jsm/lines/LineSegments2.js";
|
|
48
|
+
import { LineSegmentsGeometry } from "three/examples/jsm/lines/LineSegmentsGeometry.js";
|
|
49
|
+
|
|
50
|
+
/** 朝向相机那几棱:绿色实线 */
|
|
51
|
+
const FRONT_COLOR = 0x00ff00;
|
|
52
|
+
/** 被盒子挡住的那几棱:红色虚线(dash/gap 各 5,与参考实现同档) */
|
|
53
|
+
const BACK_COLOR = 0xff0000;
|
|
54
|
+
/** 棱线粗细(屏幕像素,`worldUnits:false` ⇒ 远近一样粗) */
|
|
55
|
+
const LINE_WIDTH_PX = 2;
|
|
56
|
+
|
|
57
|
+
export default class BoxLine2 extends Mesh {
|
|
58
|
+
constructor() {
|
|
59
|
+
super();
|
|
60
|
+
// 拾取/射线都不该把高亮框算进来(见文件头 ⚠️②);内部遮挡判据另打 `this.boxMesh`,不走这条
|
|
61
|
+
this.traverse((o) => { o.userData.pickIgnored = true; });
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** 控制台识别用(本仓约定:对象要能被认出来是谁的哪一份) */
|
|
65
|
+
type = "boxLine2";
|
|
66
|
+
|
|
67
|
+
frustumCulled = false;
|
|
68
|
+
size = new Vector3(1, 1, 1);
|
|
69
|
+
|
|
70
|
+
T1 = new Vector3(); T2 = new Vector3(); T3 = new Vector3(); T4 = new Vector3();
|
|
71
|
+
B1 = new Vector3(); B2 = new Vector3(); B3 = new Vector3(); B4 = new Vector3();
|
|
72
|
+
|
|
73
|
+
frontLineMaterial = new LineMaterial({
|
|
74
|
+
color: FRONT_COLOR,
|
|
75
|
+
worldUnits: false,
|
|
76
|
+
linewidth: LINE_WIDTH_PX,
|
|
77
|
+
depthTest: true,
|
|
78
|
+
resolution: new Vector2(1, 1),
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
backLineMaterial = new LineMaterial({
|
|
82
|
+
color: BACK_COLOR,
|
|
83
|
+
worldUnits: false,
|
|
84
|
+
dashed: true,
|
|
85
|
+
dashSize: 5,
|
|
86
|
+
gapSize: 5,
|
|
87
|
+
linewidth: LINE_WIDTH_PX,
|
|
88
|
+
depthTest: true,
|
|
89
|
+
resolution: new Vector2(1, 1),
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
createLine(name: string): LineSegments2 {
|
|
93
|
+
const segmentsGeometry = new LineSegmentsGeometry();
|
|
94
|
+
segmentsGeometry.setPositions([-0.5, 0, 0, 0.5, 0, 0]);
|
|
95
|
+
const line = new LineSegments2(segmentsGeometry, this.frontLineMaterial);
|
|
96
|
+
line.name = name;
|
|
97
|
+
line.renderOrder = 100;
|
|
98
|
+
// ⚠️ 本仓必改(参考那边没有这一行):`LineSegments2.raycast` 在 `worldUnits:false` 那一路要读
|
|
99
|
+
// `raycaster.camera`(屏幕空间投影),而编辑器拾取模型用的是 `intersectObjects(models, true)` 递归打到这里、
|
|
100
|
+
// 那把射线没设 camera ⇒ three 自己 `console.error` 之后紧接着碰 `camera.projectionMatrix` 直接抛 ⇒ 选中点不动。
|
|
101
|
+
// 线段本身不是「可点的东西」,置空最省事;那颗代理盒的 `raycast` 必须留着(遮挡判据打它)。
|
|
102
|
+
line.raycast = (): void => { };
|
|
103
|
+
line.computeLineDistances();
|
|
104
|
+
this.add(line);
|
|
105
|
+
return line;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
T1_line = this.createLine("T1,T2");
|
|
109
|
+
T2_line = this.createLine("T2,T3");
|
|
110
|
+
T3_line = this.createLine("T3,T4");
|
|
111
|
+
T4_line = this.createLine("T4,T1");
|
|
112
|
+
M1_line = this.createLine("T1,B1");
|
|
113
|
+
M2_line = this.createLine("T2,B2");
|
|
114
|
+
M3_line = this.createLine("T3,B3");
|
|
115
|
+
M4_line = this.createLine("T4,B4");
|
|
116
|
+
B1_line = this.createLine("B1,B2");
|
|
117
|
+
B2_line = this.createLine("B2,B3");
|
|
118
|
+
B3_line = this.createLine("B3,B4");
|
|
119
|
+
B4_line = this.createLine("B4,B1");
|
|
120
|
+
|
|
121
|
+
/** 射线靶子(比框小一圈):`alphaTest: 1.1` ⇒ 一个像素都不画,只给下面的遮挡判据打 */
|
|
122
|
+
boxMesh = (() => {
|
|
123
|
+
const boxGeometry = new BoxGeometry(1, 1, 1);
|
|
124
|
+
const boxMesh = new Mesh(boxGeometry, new MeshBasicMaterial({
|
|
125
|
+
color: "#ff0000", side: DoubleSide, wireframe: true, alphaTest: 1.1,
|
|
126
|
+
}));
|
|
127
|
+
boxMesh.name = "遮挡判据代理盒";
|
|
128
|
+
this.add(boxMesh);
|
|
129
|
+
return boxMesh;
|
|
130
|
+
})();
|
|
131
|
+
|
|
132
|
+
/** 代理盒单边内缩量:米级盒子照参考实现减 0.1;薄一片/占位方块那种小尺寸钳到自身的一半,⛔ 不出负数 */
|
|
133
|
+
private _insetSize(v: number): number {
|
|
134
|
+
return Math.max(v - 0.1, v * 0.5);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
setSize(v: Vector3): void {
|
|
138
|
+
this.size.set(v.x, v.y, v.z);
|
|
139
|
+
|
|
140
|
+
this.boxMesh.geometry.dispose();
|
|
141
|
+
this.boxMesh.geometry = new BoxGeometry(
|
|
142
|
+
this._insetSize(v.x),
|
|
143
|
+
this._insetSize(v.y),
|
|
144
|
+
this._insetSize(v.z),
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
this.T1.set(v.x / -2, v.y / 2, v.z / -2);
|
|
148
|
+
this.T2.set(v.x / -2, v.y / 2, v.z / 2);
|
|
149
|
+
this.T3.set(v.x / 2, v.y / 2, v.z / 2);
|
|
150
|
+
this.T4.set(v.x / 2, v.y / 2, v.z / -2);
|
|
151
|
+
this.B1.set(v.x / -2, v.y / -2, v.z / -2);
|
|
152
|
+
this.B2.set(v.x / -2, v.y / -2, v.z / 2);
|
|
153
|
+
this.B3.set(v.x / 2, v.y / -2, v.z / 2);
|
|
154
|
+
this.B4.set(v.x / 2, v.y / -2, v.z / -2);
|
|
155
|
+
|
|
156
|
+
const lines: Array<{ line: LineSegments2; p1: Vector3; p2: Vector3 }> = [
|
|
157
|
+
{ line: this.T1_line, p1: this.T1, p2: this.T2 },
|
|
158
|
+
{ line: this.T2_line, p1: this.T2, p2: this.T3 },
|
|
159
|
+
{ line: this.T3_line, p1: this.T3, p2: this.T4 },
|
|
160
|
+
{ line: this.T4_line, p1: this.T4, p2: this.T1 },
|
|
161
|
+
{ line: this.M1_line, p1: this.T1, p2: this.B1 },
|
|
162
|
+
{ line: this.M2_line, p1: this.T2, p2: this.B2 },
|
|
163
|
+
{ line: this.M3_line, p1: this.T3, p2: this.B3 },
|
|
164
|
+
{ line: this.M4_line, p1: this.T4, p2: this.B4 },
|
|
165
|
+
{ line: this.B1_line, p1: this.B1, p2: this.B2 },
|
|
166
|
+
{ line: this.B2_line, p1: this.B2, p2: this.B3 },
|
|
167
|
+
{ line: this.B3_line, p1: this.B3, p2: this.B4 },
|
|
168
|
+
{ line: this.B4_line, p1: this.B4, p2: this.B1 },
|
|
169
|
+
];
|
|
170
|
+
|
|
171
|
+
lines.forEach(({ line, p1, p2 }) => {
|
|
172
|
+
line.geometry.setPositions([p1.x, p1.y, p1.z, p2.x, p2.y, p2.z]);
|
|
173
|
+
line.computeLineDistances();
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
raycaster = new Raycaster();
|
|
178
|
+
|
|
179
|
+
onBeforeRender = (
|
|
180
|
+
renderer: WebGLRenderer, _scene: Scene, camera: Camera,
|
|
181
|
+
_geometry: BufferGeometry<NormalBufferAttributes>, _material: Material, _group: Group,
|
|
182
|
+
): void => {
|
|
183
|
+
renderer.getSize(this.frontLineMaterial.resolution);
|
|
184
|
+
renderer.getSize(this.backLineMaterial.resolution);
|
|
185
|
+
|
|
186
|
+
const farVectorName: string[] = [];
|
|
187
|
+
const farVectorNames = ["T1", "T2", "T3", "T4", "B1", "B2", "B3", "B4"];
|
|
188
|
+
this.raycaster.ray.origin.copy(camera.position);
|
|
189
|
+
|
|
190
|
+
[this.T1, this.T2, this.T3, this.T4, this.B1, this.B2, this.B3, this.B4].forEach((v, i) => {
|
|
191
|
+
const direction = v.clone().applyMatrix4(this.matrixWorld).sub(camera.position);
|
|
192
|
+
this.raycaster.ray.direction.copy(direction).normalize();
|
|
193
|
+
this.raycaster.far = direction.length();
|
|
194
|
+
const intersect = this.raycaster.intersectObject(this.boxMesh);
|
|
195
|
+
if (intersect.length > 0) {
|
|
196
|
+
farVectorName.push(farVectorNames[i]);
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
[
|
|
201
|
+
this.T1_line, this.T2_line, this.T3_line, this.T4_line,
|
|
202
|
+
this.M1_line, this.M2_line, this.M3_line, this.M4_line,
|
|
203
|
+
this.B1_line, this.B2_line, this.B3_line, this.B4_line,
|
|
204
|
+
].forEach((line) => {
|
|
205
|
+
const has = farVectorName.some((name) => line.name.includes(name));
|
|
206
|
+
line.material = has ? this.backLineMaterial : this.frontLineMaterial;
|
|
207
|
+
});
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
/** 释放:12 份线段几何 + 代理盒几何 + 两份 LineMaterial + 代理盒材质(模型槽删除/换框时调用) */
|
|
211
|
+
dispose(): void {
|
|
212
|
+
for (const line of [
|
|
213
|
+
this.T1_line, this.T2_line, this.T3_line, this.T4_line,
|
|
214
|
+
this.M1_line, this.M2_line, this.M3_line, this.M4_line,
|
|
215
|
+
this.B1_line, this.B2_line, this.B3_line, this.B4_line,
|
|
216
|
+
]) line.geometry.dispose();
|
|
217
|
+
this.boxMesh.geometry.dispose();
|
|
218
|
+
(this.boxMesh.material as MeshBasicMaterial).dispose();
|
|
219
|
+
this.frontLineMaterial.dispose();
|
|
220
|
+
this.backLineMaterial.dispose();
|
|
221
|
+
}
|
|
222
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { Mesh, MeshBasicMaterial, PlaneGeometry, Texture, Vector4 } from "three";
|
|
2
|
+
/**
|
|
3
|
+
* 皇上旨 2026-10-23「添加的平面 有点隐隐的 边框, 你在代码里面写一下, 把边框的那一点点 discrad 掉」。
|
|
4
|
+
* 整张图**外圈**这一条 UV 余量在片元里一刀 `discard` 掉 —— 边框那一圈来自裁片留下的描边/柔边像素
|
|
5
|
+
* (`alphaTest` 砍不到它:那圈像素的 alpha 高于阈值 —— 0.2 / 0.9 / 0.3 这几档都留得住它,只是颜色比内容浅,于是画成实心的一圈浅边)。
|
|
6
|
+
* 只切外圈:内部那 2×2 拼缝归每片自己的 inset 采样窗口管,两把刀互不重叠。
|
|
7
|
+
* ⚠️ 这一档是**比例**不是毫米:默认平面 2×1.5 m ⇒ 横着每边切 8 mm、竖着每边切 6 mm。尺只许写在这一处。
|
|
8
|
+
*/
|
|
9
|
+
export declare const IMAGE_PLANE_EDGE_CLIP_UV = 0.004;
|
|
10
|
+
/** 图片平面的持久化数据(进 exportData / importData) */
|
|
11
|
+
export interface ImagePlaneData {
|
|
12
|
+
name?: string;
|
|
13
|
+
position: {
|
|
14
|
+
x: number;
|
|
15
|
+
y: number;
|
|
16
|
+
z: number;
|
|
17
|
+
};
|
|
18
|
+
rotation: {
|
|
19
|
+
x: number;
|
|
20
|
+
y: number;
|
|
21
|
+
z: number;
|
|
22
|
+
};
|
|
23
|
+
width: number;
|
|
24
|
+
height: number;
|
|
25
|
+
/** 原图地址(上传后返回的 origin URL,缩略图展示用) */
|
|
26
|
+
url: string;
|
|
27
|
+
/** 瓦片地址(行优先、图像左上起;张数 = 库里 `IMAGE_TILE_GRID` 的 cols×rows,10-21 旨 = 2×2=4) */
|
|
28
|
+
tiles: string[];
|
|
29
|
+
/** 原图自然像素宽(瓦片纹理实际像素,算 inset 采样窗口需要) */
|
|
30
|
+
naturalW?: number;
|
|
31
|
+
/** 原图自然像素高 */
|
|
32
|
+
naturalH?: number;
|
|
33
|
+
}
|
|
34
|
+
export declare class ImagePlane extends Mesh<PlaneGeometry, MeshBasicMaterial> {
|
|
35
|
+
private _originalColor;
|
|
36
|
+
private _uTiles;
|
|
37
|
+
private _uInset;
|
|
38
|
+
private _uValid;
|
|
39
|
+
private _data;
|
|
40
|
+
constructor(name?: string);
|
|
41
|
+
get imageWidth(): number;
|
|
42
|
+
get imageHeight(): number;
|
|
43
|
+
setPlaneSize(w: number, h: number): void;
|
|
44
|
+
setPlaneData(d: Pick<ImagePlaneData, "url" | "tiles" | "naturalW" | "naturalH">): void;
|
|
45
|
+
get tiles(): string[];
|
|
46
|
+
get url(): string;
|
|
47
|
+
get naturalW(): number | undefined;
|
|
48
|
+
get naturalH(): number | undefined;
|
|
49
|
+
/** 把上传切好的 4 张瓦片纹理 + 每片 inset 绑到材质(缺图那片传 null ⇒ 显示灰)。uTiles 用持久对象引用 ⇒ 改 value 实时生效 */
|
|
50
|
+
setTileTextures(texs: (Texture | null)[], insets: Vector4[]): void;
|
|
51
|
+
highlight(): void;
|
|
52
|
+
restoreColor(): void;
|
|
53
|
+
toData(): ImagePlaneData;
|
|
54
|
+
applyData(d: ImagePlaneData): void;
|
|
55
|
+
dispose(): void;
|
|
56
|
+
}
|
|
57
|
+
export declare const IMAGE_PLANE_DEFAULTS: {
|
|
58
|
+
width: number;
|
|
59
|
+
height: number;
|
|
60
|
+
};
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* @Description: 图片平面元素 —— 皇上旨 2026-10-20「先添加一个平面, 再给这个平面上传图片,
|
|
3
|
+
* 上传的这个图切成 4*4=16 片, 加载的时候也是一帧载入一片」→ 2026-10-21 改口「切成 2*2=4 份,
|
|
4
|
+
* 不用切成 16 份了」。片数只在库里那一个 `IMAGE_TILE_GRID` 常数上,本文件⛔ 不写数字。
|
|
5
|
+
*
|
|
6
|
+
* 设计:
|
|
7
|
+
* - 独立 Mesh(非 InstancedChild):每张图片各有自己的 4 张瓦片纹理 ⇒ 材质实例不可共用,不合批。
|
|
8
|
+
* - 材质 MeshBasicMaterial + FrontSide(皇上旨 2026-10-22「添加的平面图片元素只要单面可见就行了,双面不需要」):
|
|
9
|
+
* 平面本身不承受光照(贴图内容自带明暗)。单面 ⇒ 背面既不画也打不中(three 的射线拾取按材质 side 剔背面,
|
|
10
|
+
* `node_modules/three/src/objects/Mesh.js:343`),与渲染共用同一条式子 ⇒ 站在这一面的反面既看不见也选不中。
|
|
11
|
+
* - 几何 PlaneGeometry(width, height),原点在平面中心;position.y 由调用者设定(通常 height/2 让底部落地)。
|
|
12
|
+
* - ⭐ 2026-10-22 晚旨:纹理**不走拼合器 RT**,直接把上传切好的 4 张瓦片纹理绑到材质,片元按 UV 象限选片 +
|
|
13
|
+
* 用每片 `rtStitchTileInset` 窗口采样(与上传切片共用同一份几何 ⇒ 接缝不回潮)。材质用 onBeforeCompile 打补丁,
|
|
14
|
+
* 注入 4 个 sampler + 每片 inset + 就绪标志;缺失那片显示灰、逐张就绪逐张显影。
|
|
15
|
+
* - UV 标准 [0,1]²(PlaneGeometry 自带),4 张瓦片按 2×2 象限铺满 ⇒ 不需要投影 shader,也不需要离屏 RT。
|
|
16
|
+
*/
|
|
17
|
+
import { Color, DataTexture, FrontSide, Mesh, MeshBasicMaterial, PlaneGeometry, RGBAFormat, Vector4, } from "three";
|
|
18
|
+
/*
|
|
19
|
+
* ⭐ 2026-10-22 晚旨:图片平面**不用拼合器 RT**,直接把上传切好的 4 张瓦片纹理绑到材质,
|
|
20
|
+
* 片元按 UV 象限选片 + 用每片 `rtStitchTileInset` 窗口采样(与上传切片共用同一份几何 ⇒ 接缝不回潮)。
|
|
21
|
+
* 占位 1×1 灰纹理:sampler 必须绑定有效纹理(未就绪片绑定它、但片元 `uValid` 为 0 时不采样它)。
|
|
22
|
+
*/
|
|
23
|
+
const PLACEHOLDER_TEX = (() => {
|
|
24
|
+
const t = new DataTexture(new Uint8Array([204, 204, 204, 255]), 1, 1, RGBAFormat);
|
|
25
|
+
t.needsUpdate = true;
|
|
26
|
+
return t;
|
|
27
|
+
})();
|
|
28
|
+
const ZERO_INSET = new Vector4(0, 0, 1, 1);
|
|
29
|
+
/**
|
|
30
|
+
* 皇上旨 2026-10-23「添加的平面 有点隐隐的 边框, 你在代码里面写一下, 把边框的那一点点 discrad 掉」。
|
|
31
|
+
* 整张图**外圈**这一条 UV 余量在片元里一刀 `discard` 掉 —— 边框那一圈来自裁片留下的描边/柔边像素
|
|
32
|
+
* (`alphaTest` 砍不到它:那圈像素的 alpha 高于阈值 —— 0.2 / 0.9 / 0.3 这几档都留得住它,只是颜色比内容浅,于是画成实心的一圈浅边)。
|
|
33
|
+
* 只切外圈:内部那 2×2 拼缝归每片自己的 inset 采样窗口管,两把刀互不重叠。
|
|
34
|
+
* ⚠️ 这一档是**比例**不是毫米:默认平面 2×1.5 m ⇒ 横着每边切 8 mm、竖着每边切 6 mm。尺只许写在这一处。
|
|
35
|
+
*/
|
|
36
|
+
export const IMAGE_PLANE_EDGE_CLIP_UV = 0.004;
|
|
37
|
+
const EDGE_MAX_UV = 1 - IMAGE_PLANE_EDGE_CLIP_UV;
|
|
38
|
+
/** 片元打补丁:4 张瓦片按 UV 象限选片,缺图那片显示灰(与旧缺失灰同一观感)。索引用字面量 0..3 ⇒ GLSL ES 1.00 合法。 */
|
|
39
|
+
const IMG_PLANE_MAP_FRAG = `
|
|
40
|
+
if ( vUv.x < ${IMAGE_PLANE_EDGE_CLIP_UV} || vUv.x > ${EDGE_MAX_UV} || vUv.y < ${IMAGE_PLANE_EDGE_CLIP_UV} || vUv.y > ${EDGE_MAX_UV} ) discard;
|
|
41
|
+
vec4 imgTexel = vec4(0.8, 0.8, 0.8, 1.0); // 缺图灰
|
|
42
|
+
vec2 cell = floor(vUv * 2.0);
|
|
43
|
+
vec2 local = fract(vUv * 2.0);
|
|
44
|
+
if (cell.y >= 0.5 && cell.x < 0.5) {
|
|
45
|
+
if (uValid[0] > 0.5) { vec4 ins = uInset[0]; imgTexel = texture2D(uTiles[0], mix(ins.xy, ins.zw, local)); }
|
|
46
|
+
} else if (cell.y >= 0.5 && cell.x >= 0.5) {
|
|
47
|
+
if (uValid[1] > 0.5) { vec4 ins = uInset[1]; imgTexel = texture2D(uTiles[1], mix(ins.xy, ins.zw, local)); }
|
|
48
|
+
} else if (cell.y < 0.5 && cell.x < 0.5) {
|
|
49
|
+
if (uValid[2] > 0.5) { vec4 ins = uInset[2]; imgTexel = texture2D(uTiles[2], mix(ins.xy, ins.zw, local)); }
|
|
50
|
+
} else {
|
|
51
|
+
if (uValid[3] > 0.5) { vec4 ins = uInset[3]; imgTexel = texture2D(uTiles[3], mix(ins.xy, ins.zw, local)); }
|
|
52
|
+
}
|
|
53
|
+
diffuseColor *= imgTexel;
|
|
54
|
+
`;
|
|
55
|
+
const DEFAULT_WIDTH = 2;
|
|
56
|
+
const DEFAULT_HEIGHT = 1.5;
|
|
57
|
+
const HIGHLIGHT_COLOR = "#ffd24a";
|
|
58
|
+
export class ImagePlane extends Mesh {
|
|
59
|
+
_originalColor = "#ffffff";
|
|
60
|
+
_uTiles;
|
|
61
|
+
_uInset;
|
|
62
|
+
_uValid;
|
|
63
|
+
_data = {
|
|
64
|
+
url: "",
|
|
65
|
+
tiles: [],
|
|
66
|
+
};
|
|
67
|
+
constructor(name) {
|
|
68
|
+
const geo = new PlaneGeometry(DEFAULT_WIDTH, DEFAULT_HEIGHT);
|
|
69
|
+
const mat = new MeshBasicMaterial({
|
|
70
|
+
color: new Color("#ffffff"),
|
|
71
|
+
side: FrontSide,
|
|
72
|
+
// 皇上旨 2026-10-21「平面的材质需要设置 alphaTest = 0.2」⇒ 同日 10-23 两次改口(先 0.9、后 **0.3**):
|
|
73
|
+
// alpha 不足阈值的像素丢弃,其余实心。0.9 那一档会把图上半透的内容也切掉,现停在 0.3。
|
|
74
|
+
// ⛔ 这个构造里不许再写「走半透明通道」与「不透明度数值」那两行:皇上 10-22 点名「没有说过要设置透明度」,
|
|
75
|
+
// 那是臣 10-21 建这个类时擅自加的(没问过),当日整条删除。平面恒走不透明通道,
|
|
76
|
+
// 要「透」只能靠 alphaTest 这一刀切掉。
|
|
77
|
+
alphaTest: 0.3,
|
|
78
|
+
});
|
|
79
|
+
// ⭐ 2026-10-22 晚旨:图片平面**不用拼合器 RT**,直接把上传切好的 4 张瓦片纹理绑到材质,
|
|
80
|
+
// 片元按 UV 象限选片 + 用每片 `rtStitchTileInset` 窗口采样(与上传切片共用同一份几何 ⇒ 接缝不回潮)。
|
|
81
|
+
// 材质打补丁(onBeforeCompile)注入 4 个 sampler + 每片 inset + 就绪标志;缺失那片显示灰。
|
|
82
|
+
mat.defines = { USE_UV: "" };
|
|
83
|
+
const uTiles = { value: [PLACEHOLDER_TEX, PLACEHOLDER_TEX, PLACEHOLDER_TEX, PLACEHOLDER_TEX] };
|
|
84
|
+
const uInset = { value: [ZERO_INSET.clone(), ZERO_INSET.clone(), ZERO_INSET.clone(), ZERO_INSET.clone()] };
|
|
85
|
+
const uValid = { value: [0, 0, 0, 0] };
|
|
86
|
+
mat.onBeforeCompile = (shader) => {
|
|
87
|
+
shader.uniforms.uTiles = uTiles;
|
|
88
|
+
shader.uniforms.uInset = uInset;
|
|
89
|
+
shader.uniforms.uValid = uValid;
|
|
90
|
+
shader.fragmentShader = shader.fragmentShader
|
|
91
|
+
.replace("#include <common>", `#include <common>
|
|
92
|
+
uniform sampler2D uTiles[4];
|
|
93
|
+
uniform vec4 uInset[4];
|
|
94
|
+
uniform float uValid[4];`)
|
|
95
|
+
.replace("#include <map_fragment>", IMG_PLANE_MAP_FRAG);
|
|
96
|
+
};
|
|
97
|
+
super(geo, mat);
|
|
98
|
+
this._uTiles = uTiles;
|
|
99
|
+
this._uInset = uInset;
|
|
100
|
+
this._uValid = uValid;
|
|
101
|
+
this.name = name || "图片平面";
|
|
102
|
+
}
|
|
103
|
+
get imageWidth() { return this.geometry.parameters.width; }
|
|
104
|
+
get imageHeight() { return this.geometry.parameters.height; }
|
|
105
|
+
setPlaneSize(w, h) {
|
|
106
|
+
this.geometry.dispose();
|
|
107
|
+
this.geometry = new PlaneGeometry(w, h);
|
|
108
|
+
}
|
|
109
|
+
setPlaneData(d) {
|
|
110
|
+
this._data = { url: d.url, tiles: d.tiles, naturalW: d.naturalW, naturalH: d.naturalH };
|
|
111
|
+
}
|
|
112
|
+
get tiles() { return this._data.tiles; }
|
|
113
|
+
get url() { return this._data.url; }
|
|
114
|
+
get naturalW() { return this._data.naturalW; }
|
|
115
|
+
get naturalH() { return this._data.naturalH; }
|
|
116
|
+
/** 把上传切好的 4 张瓦片纹理 + 每片 inset 绑到材质(缺图那片传 null ⇒ 显示灰)。uTiles 用持久对象引用 ⇒ 改 value 实时生效 */
|
|
117
|
+
setTileTextures(texs, insets) {
|
|
118
|
+
for (let i = 0; i < 4; i++) {
|
|
119
|
+
const t = texs[i] || null;
|
|
120
|
+
this._uTiles.value[i] = t || PLACEHOLDER_TEX;
|
|
121
|
+
if (insets[i])
|
|
122
|
+
this._uInset.value[i].copy(insets[i]);
|
|
123
|
+
this._uValid.value[i] = t ? 1 : 0;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
highlight() {
|
|
127
|
+
this.material.color.set(HIGHLIGHT_COLOR);
|
|
128
|
+
}
|
|
129
|
+
restoreColor() {
|
|
130
|
+
this.material.color.set(this._originalColor);
|
|
131
|
+
}
|
|
132
|
+
toData() {
|
|
133
|
+
return {
|
|
134
|
+
name: this.name || undefined,
|
|
135
|
+
position: { x: this.position.x, y: this.position.y, z: this.position.z },
|
|
136
|
+
rotation: { x: this.rotation.x, y: this.rotation.y, z: this.rotation.z },
|
|
137
|
+
width: this.imageWidth,
|
|
138
|
+
height: this.imageHeight,
|
|
139
|
+
url: this._data.url,
|
|
140
|
+
tiles: this._data.tiles,
|
|
141
|
+
naturalW: this._data.naturalW,
|
|
142
|
+
naturalH: this._data.naturalH,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
applyData(d) {
|
|
146
|
+
if (d.name)
|
|
147
|
+
this.name = d.name;
|
|
148
|
+
this.position.set(d.position.x, d.position.y, d.position.z);
|
|
149
|
+
this.rotation.set(d.rotation.x, d.rotation.y, d.rotation.z);
|
|
150
|
+
this.setPlaneSize(d.width, d.height);
|
|
151
|
+
this._data = { url: d.url, tiles: d.tiles || [], naturalW: d.naturalW, naturalH: d.naturalH };
|
|
152
|
+
}
|
|
153
|
+
dispose() {
|
|
154
|
+
this.geometry.dispose();
|
|
155
|
+
this.material.dispose();
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
export const IMAGE_PLANE_DEFAULTS = { width: DEFAULT_WIDTH, height: DEFAULT_HEIGHT };
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* @Description: 图片平面元素 —— 皇上旨 2026-10-20「先添加一个平面, 再给这个平面上传图片,
|
|
3
|
+
* 上传的这个图切成 4*4=16 片, 加载的时候也是一帧载入一片」→ 2026-10-21 改口「切成 2*2=4 份,
|
|
4
|
+
* 不用切成 16 份了」。片数只在库里那一个 `IMAGE_TILE_GRID` 常数上,本文件⛔ 不写数字。
|
|
5
|
+
*
|
|
6
|
+
* 设计:
|
|
7
|
+
* - 独立 Mesh(非 InstancedChild):每张图片各有自己的 4 张瓦片纹理 ⇒ 材质实例不可共用,不合批。
|
|
8
|
+
* - 材质 MeshBasicMaterial + FrontSide(皇上旨 2026-10-22「添加的平面图片元素只要单面可见就行了,双面不需要」):
|
|
9
|
+
* 平面本身不承受光照(贴图内容自带明暗)。单面 ⇒ 背面既不画也打不中(three 的射线拾取按材质 side 剔背面,
|
|
10
|
+
* `node_modules/three/src/objects/Mesh.js:343`),与渲染共用同一条式子 ⇒ 站在这一面的反面既看不见也选不中。
|
|
11
|
+
* - 几何 PlaneGeometry(width, height),原点在平面中心;position.y 由调用者设定(通常 height/2 让底部落地)。
|
|
12
|
+
* - ⭐ 2026-10-22 晚旨:纹理**不走拼合器 RT**,直接把上传切好的 4 张瓦片纹理绑到材质,片元按 UV 象限选片 +
|
|
13
|
+
* 用每片 `rtStitchTileInset` 窗口采样(与上传切片共用同一份几何 ⇒ 接缝不回潮)。材质用 onBeforeCompile 打补丁,
|
|
14
|
+
* 注入 4 个 sampler + 每片 inset + 就绪标志;缺失那片显示灰、逐张就绪逐张显影。
|
|
15
|
+
* - UV 标准 [0,1]²(PlaneGeometry 自带),4 张瓦片按 2×2 象限铺满 ⇒ 不需要投影 shader,也不需要离屏 RT。
|
|
16
|
+
*/
|
|
17
|
+
import {
|
|
18
|
+
Color,
|
|
19
|
+
DataTexture,
|
|
20
|
+
FrontSide,
|
|
21
|
+
Mesh,
|
|
22
|
+
MeshBasicMaterial,
|
|
23
|
+
PlaneGeometry,
|
|
24
|
+
RGBAFormat,
|
|
25
|
+
Texture,
|
|
26
|
+
Vector4,
|
|
27
|
+
} from "three";
|
|
28
|
+
|
|
29
|
+
/*
|
|
30
|
+
* ⭐ 2026-10-22 晚旨:图片平面**不用拼合器 RT**,直接把上传切好的 4 张瓦片纹理绑到材质,
|
|
31
|
+
* 片元按 UV 象限选片 + 用每片 `rtStitchTileInset` 窗口采样(与上传切片共用同一份几何 ⇒ 接缝不回潮)。
|
|
32
|
+
* 占位 1×1 灰纹理:sampler 必须绑定有效纹理(未就绪片绑定它、但片元 `uValid` 为 0 时不采样它)。
|
|
33
|
+
*/
|
|
34
|
+
const PLACEHOLDER_TEX: Texture = (() => {
|
|
35
|
+
const t = new DataTexture(new Uint8Array([204, 204, 204, 255]), 1, 1, RGBAFormat);
|
|
36
|
+
t.needsUpdate = true;
|
|
37
|
+
return t;
|
|
38
|
+
})();
|
|
39
|
+
const ZERO_INSET = new Vector4(0, 0, 1, 1);
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* 皇上旨 2026-10-23「添加的平面 有点隐隐的 边框, 你在代码里面写一下, 把边框的那一点点 discrad 掉」。
|
|
43
|
+
* 整张图**外圈**这一条 UV 余量在片元里一刀 `discard` 掉 —— 边框那一圈来自裁片留下的描边/柔边像素
|
|
44
|
+
* (`alphaTest` 砍不到它:那圈像素的 alpha 高于阈值 —— 0.2 / 0.9 / 0.3 这几档都留得住它,只是颜色比内容浅,于是画成实心的一圈浅边)。
|
|
45
|
+
* 只切外圈:内部那 2×2 拼缝归每片自己的 inset 采样窗口管,两把刀互不重叠。
|
|
46
|
+
* ⚠️ 这一档是**比例**不是毫米:默认平面 2×1.5 m ⇒ 横着每边切 8 mm、竖着每边切 6 mm。尺只许写在这一处。
|
|
47
|
+
*/
|
|
48
|
+
export const IMAGE_PLANE_EDGE_CLIP_UV = 0.004;
|
|
49
|
+
const EDGE_MAX_UV = 1 - IMAGE_PLANE_EDGE_CLIP_UV;
|
|
50
|
+
|
|
51
|
+
/** 片元打补丁:4 张瓦片按 UV 象限选片,缺图那片显示灰(与旧缺失灰同一观感)。索引用字面量 0..3 ⇒ GLSL ES 1.00 合法。 */
|
|
52
|
+
const IMG_PLANE_MAP_FRAG = `
|
|
53
|
+
if ( vUv.x < ${IMAGE_PLANE_EDGE_CLIP_UV} || vUv.x > ${EDGE_MAX_UV} || vUv.y < ${IMAGE_PLANE_EDGE_CLIP_UV} || vUv.y > ${EDGE_MAX_UV} ) discard;
|
|
54
|
+
vec4 imgTexel = vec4(0.8, 0.8, 0.8, 1.0); // 缺图灰
|
|
55
|
+
vec2 cell = floor(vUv * 2.0);
|
|
56
|
+
vec2 local = fract(vUv * 2.0);
|
|
57
|
+
if (cell.y >= 0.5 && cell.x < 0.5) {
|
|
58
|
+
if (uValid[0] > 0.5) { vec4 ins = uInset[0]; imgTexel = texture2D(uTiles[0], mix(ins.xy, ins.zw, local)); }
|
|
59
|
+
} else if (cell.y >= 0.5 && cell.x >= 0.5) {
|
|
60
|
+
if (uValid[1] > 0.5) { vec4 ins = uInset[1]; imgTexel = texture2D(uTiles[1], mix(ins.xy, ins.zw, local)); }
|
|
61
|
+
} else if (cell.y < 0.5 && cell.x < 0.5) {
|
|
62
|
+
if (uValid[2] > 0.5) { vec4 ins = uInset[2]; imgTexel = texture2D(uTiles[2], mix(ins.xy, ins.zw, local)); }
|
|
63
|
+
} else {
|
|
64
|
+
if (uValid[3] > 0.5) { vec4 ins = uInset[3]; imgTexel = texture2D(uTiles[3], mix(ins.xy, ins.zw, local)); }
|
|
65
|
+
}
|
|
66
|
+
diffuseColor *= imgTexel;
|
|
67
|
+
`;
|
|
68
|
+
|
|
69
|
+
/** 图片平面的持久化数据(进 exportData / importData) */
|
|
70
|
+
export interface ImagePlaneData {
|
|
71
|
+
name?: string;
|
|
72
|
+
position: { x: number; y: number; z: number };
|
|
73
|
+
rotation: { x: number; y: number; z: number };
|
|
74
|
+
width: number;
|
|
75
|
+
height: number;
|
|
76
|
+
/** 原图地址(上传后返回的 origin URL,缩略图展示用) */
|
|
77
|
+
url: string;
|
|
78
|
+
/** 瓦片地址(行优先、图像左上起;张数 = 库里 `IMAGE_TILE_GRID` 的 cols×rows,10-21 旨 = 2×2=4) */
|
|
79
|
+
tiles: string[];
|
|
80
|
+
/** 原图自然像素宽(瓦片纹理实际像素,算 inset 采样窗口需要) */
|
|
81
|
+
naturalW?: number;
|
|
82
|
+
/** 原图自然像素高 */
|
|
83
|
+
naturalH?: number;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const DEFAULT_WIDTH = 2;
|
|
87
|
+
const DEFAULT_HEIGHT = 1.5;
|
|
88
|
+
const HIGHLIGHT_COLOR = "#ffd24a";
|
|
89
|
+
|
|
90
|
+
export class ImagePlane extends Mesh<PlaneGeometry, MeshBasicMaterial> {
|
|
91
|
+
private _originalColor: string = "#ffffff";
|
|
92
|
+
private _uTiles!: { value: Texture[] };
|
|
93
|
+
private _uInset!: { value: Vector4[] };
|
|
94
|
+
private _uValid!: { value: number[] };
|
|
95
|
+
private _data: Omit<ImagePlaneData, "position" | "rotation" | "width" | "height"> = {
|
|
96
|
+
url: "",
|
|
97
|
+
tiles: [],
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
constructor(name?: string) {
|
|
101
|
+
const geo = new PlaneGeometry(DEFAULT_WIDTH, DEFAULT_HEIGHT);
|
|
102
|
+
const mat = new MeshBasicMaterial({
|
|
103
|
+
color: new Color("#ffffff"),
|
|
104
|
+
side: FrontSide,
|
|
105
|
+
// 皇上旨 2026-10-21「平面的材质需要设置 alphaTest = 0.2」⇒ 同日 10-23 两次改口(先 0.9、后 **0.3**):
|
|
106
|
+
// alpha 不足阈值的像素丢弃,其余实心。0.9 那一档会把图上半透的内容也切掉,现停在 0.3。
|
|
107
|
+
// ⛔ 这个构造里不许再写「走半透明通道」与「不透明度数值」那两行:皇上 10-22 点名「没有说过要设置透明度」,
|
|
108
|
+
// 那是臣 10-21 建这个类时擅自加的(没问过),当日整条删除。平面恒走不透明通道,
|
|
109
|
+
// 要「透」只能靠 alphaTest 这一刀切掉。
|
|
110
|
+
alphaTest: 0.3,
|
|
111
|
+
});
|
|
112
|
+
// ⭐ 2026-10-22 晚旨:图片平面**不用拼合器 RT**,直接把上传切好的 4 张瓦片纹理绑到材质,
|
|
113
|
+
// 片元按 UV 象限选片 + 用每片 `rtStitchTileInset` 窗口采样(与上传切片共用同一份几何 ⇒ 接缝不回潮)。
|
|
114
|
+
// 材质打补丁(onBeforeCompile)注入 4 个 sampler + 每片 inset + 就绪标志;缺失那片显示灰。
|
|
115
|
+
mat.defines = { USE_UV: "" };
|
|
116
|
+
const uTiles = { value: [PLACEHOLDER_TEX, PLACEHOLDER_TEX, PLACEHOLDER_TEX, PLACEHOLDER_TEX] as Texture[] };
|
|
117
|
+
const uInset = { value: [ZERO_INSET.clone(), ZERO_INSET.clone(), ZERO_INSET.clone(), ZERO_INSET.clone()] };
|
|
118
|
+
const uValid = { value: [0, 0, 0, 0] };
|
|
119
|
+
mat.onBeforeCompile = (shader) => {
|
|
120
|
+
shader.uniforms.uTiles = uTiles;
|
|
121
|
+
shader.uniforms.uInset = uInset;
|
|
122
|
+
shader.uniforms.uValid = uValid;
|
|
123
|
+
shader.fragmentShader = shader.fragmentShader
|
|
124
|
+
.replace(
|
|
125
|
+
"#include <common>",
|
|
126
|
+
`#include <common>
|
|
127
|
+
uniform sampler2D uTiles[4];
|
|
128
|
+
uniform vec4 uInset[4];
|
|
129
|
+
uniform float uValid[4];`
|
|
130
|
+
)
|
|
131
|
+
.replace("#include <map_fragment>", IMG_PLANE_MAP_FRAG);
|
|
132
|
+
};
|
|
133
|
+
super(geo, mat);
|
|
134
|
+
this._uTiles = uTiles;
|
|
135
|
+
this._uInset = uInset;
|
|
136
|
+
this._uValid = uValid;
|
|
137
|
+
this.name = name || "图片平面";
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
get imageWidth(): number { return (this.geometry as PlaneGeometry).parameters.width; }
|
|
141
|
+
get imageHeight(): number { return (this.geometry as PlaneGeometry).parameters.height; }
|
|
142
|
+
|
|
143
|
+
setPlaneSize(w: number, h: number): void {
|
|
144
|
+
this.geometry.dispose();
|
|
145
|
+
this.geometry = new PlaneGeometry(w, h);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
setPlaneData(d: Pick<ImagePlaneData, "url" | "tiles" | "naturalW" | "naturalH">): void {
|
|
149
|
+
this._data = { url: d.url, tiles: d.tiles, naturalW: d.naturalW, naturalH: d.naturalH };
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
get tiles(): string[] { return this._data.tiles; }
|
|
153
|
+
get url(): string { return this._data.url; }
|
|
154
|
+
get naturalW(): number | undefined { return this._data.naturalW; }
|
|
155
|
+
get naturalH(): number | undefined { return this._data.naturalH; }
|
|
156
|
+
|
|
157
|
+
/** 把上传切好的 4 张瓦片纹理 + 每片 inset 绑到材质(缺图那片传 null ⇒ 显示灰)。uTiles 用持久对象引用 ⇒ 改 value 实时生效 */
|
|
158
|
+
setTileTextures(texs: (Texture | null)[], insets: Vector4[]): void {
|
|
159
|
+
for (let i = 0; i < 4; i++) {
|
|
160
|
+
const t = texs[i] || null;
|
|
161
|
+
this._uTiles.value[i] = t || PLACEHOLDER_TEX;
|
|
162
|
+
if (insets[i]) this._uInset.value[i].copy(insets[i]);
|
|
163
|
+
this._uValid.value[i] = t ? 1 : 0;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
highlight(): void {
|
|
168
|
+
this.material.color.set(HIGHLIGHT_COLOR);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
restoreColor(): void {
|
|
172
|
+
this.material.color.set(this._originalColor);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
toData(): ImagePlaneData {
|
|
176
|
+
return {
|
|
177
|
+
name: this.name || undefined,
|
|
178
|
+
position: { x: this.position.x, y: this.position.y, z: this.position.z },
|
|
179
|
+
rotation: { x: this.rotation.x, y: this.rotation.y, z: this.rotation.z },
|
|
180
|
+
width: this.imageWidth,
|
|
181
|
+
height: this.imageHeight,
|
|
182
|
+
url: this._data.url,
|
|
183
|
+
tiles: this._data.tiles,
|
|
184
|
+
naturalW: this._data.naturalW,
|
|
185
|
+
naturalH: this._data.naturalH,
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
applyData(d: ImagePlaneData): void {
|
|
190
|
+
if (d.name) this.name = d.name;
|
|
191
|
+
this.position.set(d.position.x, d.position.y, d.position.z);
|
|
192
|
+
this.rotation.set(d.rotation.x, d.rotation.y, d.rotation.z);
|
|
193
|
+
this.setPlaneSize(d.width, d.height);
|
|
194
|
+
this._data = { url: d.url, tiles: d.tiles || [], naturalW: d.naturalW, naturalH: d.naturalH };
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
dispose(): void {
|
|
198
|
+
this.geometry.dispose();
|
|
199
|
+
this.material.dispose();
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
export const IMAGE_PLANE_DEFAULTS = { width: DEFAULT_WIDTH, height: DEFAULT_HEIGHT };
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { Texture, Vector4 } from "three";
|
|
2
|
+
interface TileEntry {
|
|
3
|
+
tex: Texture | null;
|
|
4
|
+
w: number;
|
|
5
|
+
h: number;
|
|
6
|
+
}
|
|
7
|
+
export declare class ImagePlaneTileLoader {
|
|
8
|
+
private cache;
|
|
9
|
+
private inflight;
|
|
10
|
+
/** 触发某张瓦片下载 + 解码(已缓存 / 在途则跳过);完成后填入 cache,下一帧 feed 即取到 */
|
|
11
|
+
preload(url: string): void;
|
|
12
|
+
/** 取已就绪的瓦片(未就绪返回 null) */
|
|
13
|
+
get(url: string): TileEntry | null;
|
|
14
|
+
/** 第 i 片(行优先、图像左上起)的采样窗口;瓦片未就绪时返回整张 [0,0,1,1] */
|
|
15
|
+
inset(entry: TileEntry, i: number): Vector4;
|
|
16
|
+
/** 释放本 loader 持有的全部瓦片纹理(项目重置 / 卸载场景时调用;⛔ 不关共享占位纹理) */
|
|
17
|
+
clear(): void;
|
|
18
|
+
}
|
|
19
|
+
export {};
|