easy-threesdk 1.0.0 → 1.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/index.js +4 -2
- package/loader/WaterMaterial/WaterEffect.js +255 -0
- package/loader/WaterMaterial/index.js +36 -0
- package/loader/WaterMaterial/resource/shaders/caustics/fragment.glsl +24 -0
- package/loader/WaterMaterial/resource/shaders/caustics/vertex.glsl +34 -0
- package/loader/WaterMaterial/resource/shaders/debug/fragment.glsl +12 -0
- package/loader/WaterMaterial/resource/shaders/debug/vertex.glsl +10 -0
- package/loader/WaterMaterial/resource/shaders/pool/fragment.glsl +17 -0
- package/loader/WaterMaterial/resource/shaders/pool/vertex.glsl +16 -0
- package/loader/WaterMaterial/resource/shaders/simulation/drop_fragment.glsl +22 -0
- package/loader/WaterMaterial/resource/shaders/simulation/normal_fragment.glsl +19 -0
- package/loader/WaterMaterial/resource/shaders/simulation/update_fragment.glsl +33 -0
- package/loader/WaterMaterial/resource/shaders/simulation/vertex.glsl +9 -0
- package/loader/WaterMaterial/resource/shaders/utils.glsl +60 -0
- package/loader/WaterMaterial/resource/shaders/water/fragment.glsl +69 -0
- package/loader/WaterMaterial/resource/shaders/water/vertex.glsl +24 -0
- package/loader/WaterMaterial/resource/tiles.jpg +0 -0
- package/loader/WaterMaterial/resource/vite.svg +1 -0
- package/loader/WaterMaterial/resource/xneg.jpg +0 -0
- package/loader/WaterMaterial/resource/xpos.jpg +0 -0
- package/loader/WaterMaterial/resource/ypos.jpg +0 -0
- package/loader/WaterMaterial/resource/zneg.jpg +0 -0
- package/loader/WaterMaterial/resource/zpos.jpg +0 -0
- package/package.json +5 -1
- package/plugins/LabelControl.js +283 -6
- package/plugins/ModelControl.js +8 -11
package/index.js
CHANGED
|
@@ -156,7 +156,8 @@ class ThreeSdk {
|
|
|
156
156
|
const height = this.container.clientHeight;
|
|
157
157
|
|
|
158
158
|
//初始化摄像机
|
|
159
|
-
|
|
159
|
+
//注意在创建球体场景中我们设置球体场景的半径为1000,如果创建了球体场景那么我们相机的远裁剪面要远大于这个球体的半径不然屏幕中间会出现“黑洞”。所以保险起见我们对于相机的远裁剪面设置为比较大的10000。
|
|
160
|
+
this.camera = new THREE.PerspectiveCamera(75, width / height, 0.1, 10000);
|
|
160
161
|
this.camera.position.set(position.x, position.y, position.z);
|
|
161
162
|
this.camera.lookAt(lookAt.x, lookAt.y, lookAt.z);
|
|
162
163
|
|
|
@@ -262,7 +263,7 @@ class ThreeSdk {
|
|
|
262
263
|
const point = groundIntersects[0].point;
|
|
263
264
|
this._RoamConfig.groundCircle.position.set(
|
|
264
265
|
point.x,
|
|
265
|
-
point.y + 0.01,
|
|
266
|
+
point.y + 0.01, //这个圆圈显示的高度位置具体需要根据你创建的模型的地形高度来设置,根据你加载的模型多测几次微调几遍就行了
|
|
266
267
|
point.z
|
|
267
268
|
); // 稍微抬高避免z-fighting
|
|
268
269
|
this._RoamConfig.groundCircle.visible = true;
|
|
@@ -502,6 +503,7 @@ class ThreeSdk {
|
|
|
502
503
|
const target = this.camera.position
|
|
503
504
|
.clone()
|
|
504
505
|
.add(direction.multiplyScalar(10));
|
|
506
|
+
|
|
505
507
|
this.camera.lookAt(target);
|
|
506
508
|
}
|
|
507
509
|
} else {
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
import * as THREE from 'three';
|
|
2
|
+
|
|
3
|
+
// Helper to load shader files
|
|
4
|
+
function loadFile(filename) {
|
|
5
|
+
return new Promise((resolve, reject) => {
|
|
6
|
+
const loader = new THREE.FileLoader();
|
|
7
|
+
loader.load(filename, (data) => {
|
|
8
|
+
resolve(data);
|
|
9
|
+
}, undefined, reject);
|
|
10
|
+
});
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const defaultLight = [0.7559289460184544, 0.7559289460184544, -0.3779644730092272];
|
|
14
|
+
|
|
15
|
+
class WaterSimulation {
|
|
16
|
+
constructor() {
|
|
17
|
+
this._camera = new THREE.OrthographicCamera(0, 1, 1, 0, 0, 2000);
|
|
18
|
+
this._geometry = new THREE.PlaneGeometry(2, 2);
|
|
19
|
+
|
|
20
|
+
this._textureA = new THREE.WebGLRenderTarget(256, 256, { type: THREE.FloatType, minFilter: THREE.LinearFilter, magFilter: THREE.LinearFilter });
|
|
21
|
+
this._textureB = new THREE.WebGLRenderTarget(256, 256, { type: THREE.FloatType, minFilter: THREE.LinearFilter, magFilter: THREE.LinearFilter });
|
|
22
|
+
this.texture = this._textureA;
|
|
23
|
+
|
|
24
|
+
const shadersPromises = [
|
|
25
|
+
loadFile('./resource/shaders/simulation/vertex.glsl'),
|
|
26
|
+
loadFile('./resource/shaders/simulation/drop_fragment.glsl'),
|
|
27
|
+
loadFile('./resource/shaders/simulation/normal_fragment.glsl'),
|
|
28
|
+
loadFile('./resource/shaders/simulation/update_fragment.glsl'),
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
this.loaded = Promise.all(shadersPromises)
|
|
32
|
+
.then(([vertexShader, dropFragmentShader, normalFragmentShader, updateFragmentShader]) => {
|
|
33
|
+
const dropMaterial = new THREE.RawShaderMaterial({
|
|
34
|
+
uniforms: {
|
|
35
|
+
center: { value: [0, 0] },
|
|
36
|
+
radius: { value: 0 },
|
|
37
|
+
strength: { value: 0 },
|
|
38
|
+
texture: { value: null },
|
|
39
|
+
},
|
|
40
|
+
vertexShader: vertexShader,
|
|
41
|
+
fragmentShader: dropFragmentShader,
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
const normalMaterial = new THREE.RawShaderMaterial({
|
|
45
|
+
uniforms: {
|
|
46
|
+
delta: { value: [1 / 256, 1 / 256] },
|
|
47
|
+
texture: { value: null },
|
|
48
|
+
},
|
|
49
|
+
vertexShader: vertexShader,
|
|
50
|
+
fragmentShader: normalFragmentShader,
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
const updateMaterial = new THREE.RawShaderMaterial({
|
|
54
|
+
uniforms: {
|
|
55
|
+
delta: { value: [1 / 256, 1 / 256] },
|
|
56
|
+
texture: { value: null },
|
|
57
|
+
},
|
|
58
|
+
vertexShader: vertexShader,
|
|
59
|
+
fragmentShader: updateFragmentShader,
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
this._dropMesh = new THREE.Mesh(this._geometry, dropMaterial);
|
|
63
|
+
this._normalMesh = new THREE.Mesh(this._geometry, normalMaterial);
|
|
64
|
+
this._updateMesh = new THREE.Mesh(this._geometry, updateMaterial);
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
addDrop(renderer, x, y, radius, strength) {
|
|
69
|
+
if (!this._dropMesh) return;
|
|
70
|
+
this._dropMesh.material.uniforms['center'].value = [x, y];
|
|
71
|
+
this._dropMesh.material.uniforms['radius'].value = radius;
|
|
72
|
+
this._dropMesh.material.uniforms['strength'].value = strength;
|
|
73
|
+
|
|
74
|
+
this._render(renderer, this._dropMesh);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
stepSimulation(renderer) {
|
|
78
|
+
if (!this._updateMesh) return;
|
|
79
|
+
this._render(renderer, this._updateMesh);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
updateNormals(renderer) {
|
|
83
|
+
if (!this._normalMesh) return;
|
|
84
|
+
this._render(renderer, this._normalMesh);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
_render(renderer, mesh) {
|
|
88
|
+
const oldTexture = this.texture;
|
|
89
|
+
const newTexture = this.texture === this._textureA ? this._textureB : this._textureA;
|
|
90
|
+
|
|
91
|
+
mesh.material.uniforms['texture'].value = oldTexture.texture;
|
|
92
|
+
|
|
93
|
+
renderer.setRenderTarget(newTexture);
|
|
94
|
+
renderer.render(mesh, this._camera);
|
|
95
|
+
|
|
96
|
+
this.texture = newTexture;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
class Caustics {
|
|
101
|
+
constructor(lightFrontGeometry, light) {
|
|
102
|
+
this._camera = new THREE.OrthographicCamera(0, 1, 1, 0, 0, 2000);
|
|
103
|
+
this._geometry = lightFrontGeometry;
|
|
104
|
+
this.light = light;
|
|
105
|
+
|
|
106
|
+
this.texture = new THREE.WebGLRenderTarget(1024, 1024, { type: THREE.UnsignedByteType });
|
|
107
|
+
|
|
108
|
+
const shadersPromises = [
|
|
109
|
+
loadFile('./resource/shaders/caustics/vertex.glsl'),
|
|
110
|
+
loadFile('./resource/shaders/caustics/fragment.glsl')
|
|
111
|
+
];
|
|
112
|
+
|
|
113
|
+
this.loaded = Promise.all(shadersPromises)
|
|
114
|
+
.then(([vertexShader, fragmentShader]) => {
|
|
115
|
+
const material = new THREE.RawShaderMaterial({
|
|
116
|
+
uniforms: {
|
|
117
|
+
light: { value: this.light },
|
|
118
|
+
water: { value: null },
|
|
119
|
+
},
|
|
120
|
+
vertexShader: vertexShader,
|
|
121
|
+
fragmentShader: fragmentShader,
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
this._causticMesh = new THREE.Mesh(this._geometry, material);
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
update(renderer, waterTexture) {
|
|
129
|
+
if (!this._causticMesh) return;
|
|
130
|
+
this._causticMesh.material.uniforms['water'].value = waterTexture;
|
|
131
|
+
|
|
132
|
+
renderer.setRenderTarget(this.texture);
|
|
133
|
+
renderer.setClearColor(new THREE.Color('black'), 0);
|
|
134
|
+
renderer.clear();
|
|
135
|
+
|
|
136
|
+
renderer.render(this._causticMesh, this._camera);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export class WaterEffect {
|
|
141
|
+
constructor(renderer, camera, scene, options = {}) {
|
|
142
|
+
this.renderer = renderer;
|
|
143
|
+
this.camera = camera;
|
|
144
|
+
this.scene = scene;
|
|
145
|
+
this.light = options.light || defaultLight;
|
|
146
|
+
this.tiles = options.tiles;
|
|
147
|
+
this.skybox = options.skybox;
|
|
148
|
+
|
|
149
|
+
this.waterSimulation = new WaterSimulation();
|
|
150
|
+
|
|
151
|
+
// Interaction
|
|
152
|
+
this.raycaster = new THREE.Raycaster();
|
|
153
|
+
this.mouse = new THREE.Vector2();
|
|
154
|
+
this.targetGeometry = new THREE.PlaneGeometry(2, 2);
|
|
155
|
+
this.targetGeometry.rotateX(-Math.PI / 2); // Match the water plane orientation
|
|
156
|
+
this.targetMesh = new THREE.Mesh(this.targetGeometry);
|
|
157
|
+
|
|
158
|
+
// Load utils shader globally
|
|
159
|
+
loadFile('./resource/shaders/utils.glsl').then((utils) => {
|
|
160
|
+
THREE.ShaderChunk['utils'] = utils;
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
// Bind mouse event
|
|
164
|
+
window.addEventListener('mousemove', this.onMouseMove.bind(this));
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
async applyTo(mesh) {
|
|
168
|
+
this.mesh = mesh;
|
|
169
|
+
|
|
170
|
+
// Initialize Caustics with the mesh geometry
|
|
171
|
+
this.caustics = new Caustics(mesh.geometry, this.light);
|
|
172
|
+
|
|
173
|
+
const shadersPromises = [
|
|
174
|
+
loadFile('./resource/shaders/water/vertex.glsl'),
|
|
175
|
+
loadFile('./resource/shaders/water/fragment.glsl'),
|
|
176
|
+
this.waterSimulation.loaded,
|
|
177
|
+
this.caustics.loaded
|
|
178
|
+
];
|
|
179
|
+
|
|
180
|
+
const [vertexShader, fragmentShader] = await Promise.all(shadersPromises);
|
|
181
|
+
|
|
182
|
+
if (!vertexShader || !fragmentShader) {
|
|
183
|
+
console.error("Failed to load shaders");
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const material = new THREE.RawShaderMaterial({
|
|
188
|
+
uniforms: {
|
|
189
|
+
light: { value: this.light },
|
|
190
|
+
tiles: { value: this.tiles || null },
|
|
191
|
+
sky: { value: this.skybox || null },
|
|
192
|
+
water: { value: null },
|
|
193
|
+
causticTex: { value: null },
|
|
194
|
+
underwater: { value: false },
|
|
195
|
+
},
|
|
196
|
+
vertexShader: vertexShader,
|
|
197
|
+
fragmentShader: fragmentShader,
|
|
198
|
+
side: THREE.FrontSide
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
this.mesh.material = material;
|
|
202
|
+
|
|
203
|
+
// Initial drops
|
|
204
|
+
for (let i = 0; i < 20; i++) {
|
|
205
|
+
this.waterSimulation.addDrop(
|
|
206
|
+
this.renderer,
|
|
207
|
+
Math.random() * 2 - 1, Math.random() * 2 - 1,
|
|
208
|
+
0.03, (i & 1) ? 0.02 : -0.02
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
update() {
|
|
214
|
+
if (!this.mesh || !this.caustics) return;
|
|
215
|
+
|
|
216
|
+
// 1. Physics simulation
|
|
217
|
+
this.waterSimulation.stepSimulation(this.renderer);
|
|
218
|
+
this.waterSimulation.updateNormals(this.renderer);
|
|
219
|
+
const waterTexture = this.waterSimulation.texture.texture;
|
|
220
|
+
|
|
221
|
+
// 2. Caustics calculation
|
|
222
|
+
this.caustics.update(this.renderer, waterTexture);
|
|
223
|
+
const causticsTexture = this.caustics.texture.texture;
|
|
224
|
+
|
|
225
|
+
// 3. Update main mesh uniforms
|
|
226
|
+
const material = this.mesh.material;
|
|
227
|
+
material.uniforms['water'].value = waterTexture;
|
|
228
|
+
material.uniforms['causticTex'].value = causticsTexture;
|
|
229
|
+
|
|
230
|
+
// Reset render target to screen
|
|
231
|
+
this.renderer.setRenderTarget(null);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
onMouseMove(event) {
|
|
235
|
+
const rect = this.renderer.domElement.getBoundingClientRect();
|
|
236
|
+
|
|
237
|
+
this.mouse.x = (event.clientX - rect.left) * 2 / rect.width - 1;
|
|
238
|
+
this.mouse.y = - (event.clientY - rect.top) * 2 / rect.height + 1;
|
|
239
|
+
|
|
240
|
+
this.raycaster.setFromCamera(this.mouse, this.camera);
|
|
241
|
+
|
|
242
|
+
if (this.mesh) {
|
|
243
|
+
this.targetMesh.position.copy(this.mesh.position);
|
|
244
|
+
this.targetMesh.rotation.copy(this.mesh.rotation);
|
|
245
|
+
this.targetMesh.scale.copy(this.mesh.scale);
|
|
246
|
+
this.targetMesh.updateMatrixWorld();
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const intersects = this.raycaster.intersectObject(this.targetMesh);
|
|
250
|
+
|
|
251
|
+
for (let intersect of intersects) {
|
|
252
|
+
this.waterSimulation.addDrop(this.renderer, intersect.point.x, intersect.point.z, 0.03, 0.04);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
function createWaterLoader(renderer, camera, scene) {
|
|
2
|
+
// Load Textures
|
|
3
|
+
const cubetextureloader = new THREE.CubeTextureLoader();
|
|
4
|
+
const textureCube = cubetextureloader.load([
|
|
5
|
+
'/resource/xpos.jpg', '/resource/xneg.jpg',
|
|
6
|
+
'/resource/ypos.jpg', '/resource/ypos.jpg',
|
|
7
|
+
'/resource/zpos.jpg', '/resource/zneg.jpg',
|
|
8
|
+
]);
|
|
9
|
+
|
|
10
|
+
const textureloader = new THREE.TextureLoader();
|
|
11
|
+
const tiles = textureloader.load('/resource/tiles.jpg');
|
|
12
|
+
|
|
13
|
+
// Initialize Water Effect
|
|
14
|
+
const waterEffect = new WaterEffect(renderer, camera, scene, {
|
|
15
|
+
tiles: tiles,
|
|
16
|
+
skybox: textureCube
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
function animate() {
|
|
20
|
+
// Update water effect
|
|
21
|
+
waterEffect.update();
|
|
22
|
+
|
|
23
|
+
// Clear and Render Scene
|
|
24
|
+
renderer.setClearColor(new THREE.Color('white'), 1);
|
|
25
|
+
renderer.clear();
|
|
26
|
+
|
|
27
|
+
renderer.render(scene, camera);
|
|
28
|
+
|
|
29
|
+
controls.update();
|
|
30
|
+
|
|
31
|
+
requestAnimationFrame(animate);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
animate();
|
|
35
|
+
return waterEffect;
|
|
36
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
precision highp float;
|
|
2
|
+
precision highp int;
|
|
3
|
+
|
|
4
|
+
#extension GL_OES_standard_derivatives : enable
|
|
5
|
+
|
|
6
|
+
#include <utils>
|
|
7
|
+
|
|
8
|
+
varying vec3 oldPos;
|
|
9
|
+
varying vec3 newPos;
|
|
10
|
+
varying vec3 ray;
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
void main() {
|
|
14
|
+
/* if the triangle gets smaller, it gets brighter, and vice versa */
|
|
15
|
+
float oldArea = length(dFdx(oldPos)) * length(dFdy(oldPos));
|
|
16
|
+
float newArea = length(dFdx(newPos)) * length(dFdy(newPos));
|
|
17
|
+
gl_FragColor = vec4(oldArea / newArea * 0.2, 1.0, 0.0, 0.0);
|
|
18
|
+
|
|
19
|
+
vec3 refractedLight = refract(-light, vec3(0.0, 1.0, 0.0), IOR_AIR / IOR_WATER);
|
|
20
|
+
|
|
21
|
+
/* shadow for the rim of the pool */
|
|
22
|
+
vec2 t = intersectCube(newPos, -refractedLight, vec3(-1.0, -poolHeight, -1.0), vec3(1.0, 2.0, 1.0));
|
|
23
|
+
gl_FragColor.r *= 1.0 / (1.0 + exp(-200.0 / (1.0 + 10.0 * (t.y - t.x)) * (newPos.y - refractedLight.y * t.y - 2.0 / 12.0)));
|
|
24
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
precision highp float;
|
|
2
|
+
precision highp int;
|
|
3
|
+
|
|
4
|
+
varying vec3 oldPos;
|
|
5
|
+
varying vec3 newPos;
|
|
6
|
+
varying vec3 ray;
|
|
7
|
+
attribute vec3 position;
|
|
8
|
+
|
|
9
|
+
#include <utils>
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
/* project the ray onto the plane */
|
|
13
|
+
vec3 project(vec3 origin, vec3 ray, vec3 refractedLight) {
|
|
14
|
+
vec2 tcube = intersectCube(origin, ray, vec3(-1.0, -poolHeight, -1.0), vec3(1.0, 2.0, 1.0));
|
|
15
|
+
origin += ray * tcube.y;
|
|
16
|
+
float tplane = (-origin.y - 1.0) / refractedLight.y;
|
|
17
|
+
|
|
18
|
+
return origin + refractedLight * tplane;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
void main() {
|
|
23
|
+
vec4 info = texture2D(water, position.xy * 0.5 + 0.5);
|
|
24
|
+
info.ba *= 0.5;
|
|
25
|
+
vec3 normal = vec3(info.b, sqrt(1.0 - dot(info.ba, info.ba)), info.a);
|
|
26
|
+
|
|
27
|
+
/* project the vertices along the refracted vertex ray */
|
|
28
|
+
vec3 refractedLight = refract(-light, vec3(0.0, 1.0, 0.0), IOR_AIR / IOR_WATER);
|
|
29
|
+
ray = refract(-light, normal, IOR_AIR / IOR_WATER);
|
|
30
|
+
oldPos = project(position.xzy, refractedLight, refractedLight);
|
|
31
|
+
newPos = project(position.xzy + vec3(0.0, info.r, 0.0), ray, refractedLight);
|
|
32
|
+
|
|
33
|
+
gl_Position = vec4(0.75 * (newPos.xz + refractedLight.xz / refractedLight.y), 0.0, 1.0);
|
|
34
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
precision highp float;
|
|
2
|
+
precision highp int;
|
|
3
|
+
|
|
4
|
+
#include <utils>
|
|
5
|
+
|
|
6
|
+
varying vec3 pos;
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
void main() {
|
|
10
|
+
gl_FragColor = vec4(getWallColor(pos), 1.0);
|
|
11
|
+
|
|
12
|
+
vec4 info = texture2D(water, pos.xz * 0.5 + 0.5);
|
|
13
|
+
|
|
14
|
+
if (pos.y < info.r) {
|
|
15
|
+
gl_FragColor.rgb *= underwaterColor * 1.2;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
#include <utils>
|
|
2
|
+
|
|
3
|
+
uniform mat4 projectionMatrix;
|
|
4
|
+
uniform mat4 modelViewMatrix;
|
|
5
|
+
|
|
6
|
+
attribute vec3 position;
|
|
7
|
+
|
|
8
|
+
varying vec3 pos;
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
void main() {
|
|
12
|
+
pos = position.xyz;
|
|
13
|
+
pos.y = ((1.0 - pos.y) * (7.0 / 12.0) - 1.0) * poolHeight;
|
|
14
|
+
|
|
15
|
+
gl_Position = projectionMatrix * modelViewMatrix * vec4(pos, 1.0);
|
|
16
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
precision highp float;
|
|
2
|
+
precision highp int;
|
|
3
|
+
|
|
4
|
+
const float PI = 3.141592653589793;
|
|
5
|
+
uniform sampler2D texture;
|
|
6
|
+
uniform vec2 center;
|
|
7
|
+
uniform float radius;
|
|
8
|
+
uniform float strength;
|
|
9
|
+
varying vec2 coord;
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
void main() {
|
|
13
|
+
/* Get vertex info */
|
|
14
|
+
vec4 info = texture2D(texture, coord);
|
|
15
|
+
|
|
16
|
+
/* Add the drop to the height */
|
|
17
|
+
float drop = max(0.0, 1.0 - length(center * 0.5 + 0.5 - coord) / radius);
|
|
18
|
+
drop = 0.5 - cos(drop * PI) * 0.5;
|
|
19
|
+
info.r += drop * strength;
|
|
20
|
+
|
|
21
|
+
gl_FragColor = info;
|
|
22
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
precision highp float;
|
|
2
|
+
precision highp int;
|
|
3
|
+
|
|
4
|
+
uniform sampler2D texture;
|
|
5
|
+
uniform vec2 delta;
|
|
6
|
+
varying vec2 coord;
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
void main() {
|
|
10
|
+
/* get vertex info */
|
|
11
|
+
vec4 info = texture2D(texture, coord);
|
|
12
|
+
|
|
13
|
+
/* update the normal */
|
|
14
|
+
vec3 dx = vec3(delta.x, texture2D(texture, vec2(coord.x + delta.x, coord.y)).r - info.r, 0.0);
|
|
15
|
+
vec3 dy = vec3(0.0, texture2D(texture, vec2(coord.x, coord.y + delta.y)).r - info.r, delta.y);
|
|
16
|
+
info.ba = normalize(cross(dy, dx)).xz;
|
|
17
|
+
|
|
18
|
+
gl_FragColor = info;
|
|
19
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
precision highp float;
|
|
2
|
+
precision highp int;
|
|
3
|
+
|
|
4
|
+
uniform sampler2D texture;
|
|
5
|
+
uniform vec2 delta;
|
|
6
|
+
varying vec2 coord;
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
void main() {
|
|
10
|
+
/* get vertex info */
|
|
11
|
+
vec4 info = texture2D(texture, coord);
|
|
12
|
+
|
|
13
|
+
/* calculate average neighbor height */
|
|
14
|
+
vec2 dx = vec2(delta.x, 0.0);
|
|
15
|
+
vec2 dy = vec2(0.0, delta.y);
|
|
16
|
+
float average = (
|
|
17
|
+
texture2D(texture, coord - dx).r +
|
|
18
|
+
texture2D(texture, coord - dy).r +
|
|
19
|
+
texture2D(texture, coord + dx).r +
|
|
20
|
+
texture2D(texture, coord + dy).r
|
|
21
|
+
) * 0.25;
|
|
22
|
+
|
|
23
|
+
/* change the velocity to move toward the average */
|
|
24
|
+
info.g += (average - info.r) * 2.0;
|
|
25
|
+
|
|
26
|
+
/* attenuate the velocity a little so waves do not last forever */
|
|
27
|
+
info.g *= 0.995;
|
|
28
|
+
|
|
29
|
+
/* move the vertex along the velocity */
|
|
30
|
+
info.r += info.g;
|
|
31
|
+
|
|
32
|
+
gl_FragColor = info;
|
|
33
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
const float IOR_AIR = 1.0;
|
|
2
|
+
const float IOR_WATER = 1.333;
|
|
3
|
+
|
|
4
|
+
const vec3 abovewaterColor = vec3(0.25, 1.0, 1.25);
|
|
5
|
+
const vec3 underwaterColor = vec3(0.4, 0.9, 1.0);
|
|
6
|
+
|
|
7
|
+
const float poolHeight = 1.0;
|
|
8
|
+
|
|
9
|
+
uniform vec3 light;
|
|
10
|
+
uniform sampler2D tiles;
|
|
11
|
+
uniform sampler2D causticTex;
|
|
12
|
+
uniform sampler2D water;
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
vec2 intersectCube(vec3 origin, vec3 ray, vec3 cubeMin, vec3 cubeMax) {
|
|
16
|
+
vec3 tMin = (cubeMin - origin) / ray;
|
|
17
|
+
vec3 tMax = (cubeMax - origin) / ray;
|
|
18
|
+
vec3 t1 = min(tMin, tMax);
|
|
19
|
+
vec3 t2 = max(tMin, tMax);
|
|
20
|
+
float tNear = max(max(t1.x, t1.y), t1.z);
|
|
21
|
+
float tFar = min(min(t2.x, t2.y), t2.z);
|
|
22
|
+
return vec2(tNear, tFar);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
vec3 getWallColor(vec3 point) {
|
|
27
|
+
float scale = 0.5;
|
|
28
|
+
|
|
29
|
+
vec3 wallColor;
|
|
30
|
+
vec3 normal;
|
|
31
|
+
if (abs(point.x) > 0.999) {
|
|
32
|
+
wallColor = texture2D(tiles, point.yz * 0.5 + vec2(1.0, 0.5)).rgb;
|
|
33
|
+
normal = vec3(-point.x, 0.0, 0.0);
|
|
34
|
+
} else if (abs(point.z) > 0.999) {
|
|
35
|
+
wallColor = texture2D(tiles, point.yx * 0.5 + vec2(1.0, 0.5)).rgb;
|
|
36
|
+
normal = vec3(0.0, 0.0, -point.z);
|
|
37
|
+
} else {
|
|
38
|
+
wallColor = texture2D(tiles, point.xz * 0.5 + 0.5).rgb;
|
|
39
|
+
normal = vec3(0.0, 1.0, 0.0);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
scale /= length(point); /* pool ambient occlusion */
|
|
43
|
+
|
|
44
|
+
/* caustics */
|
|
45
|
+
vec3 refractedLight = -refract(-light, vec3(0.0, 1.0, 0.0), IOR_AIR / IOR_WATER);
|
|
46
|
+
float diffuse = max(0.0, dot(refractedLight, normal));
|
|
47
|
+
vec4 info = texture2D(water, point.xz * 0.5 + 0.5);
|
|
48
|
+
if (point.y < info.r) {
|
|
49
|
+
vec4 caustic = texture2D(causticTex, 0.75 * (point.xz - point.y * refractedLight.xz / refractedLight.y) * 0.5 + 0.5);
|
|
50
|
+
scale += diffuse * caustic.r * 2.0 * caustic.g;
|
|
51
|
+
} else {
|
|
52
|
+
/* shadow for the rim of the pool */
|
|
53
|
+
vec2 t = intersectCube(point, refractedLight, vec3(-1.0, -poolHeight, -1.0), vec3(1.0, 2.0, 1.0));
|
|
54
|
+
diffuse *= 1.0 / (1.0 + exp(-200.0 / (1.0 + 10.0 * (t.y - t.x)) * (point.y + refractedLight.y * t.y - 2.0 / 12.0)));
|
|
55
|
+
|
|
56
|
+
scale += diffuse * 0.5;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return wallColor * scale;
|
|
60
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
precision highp float;
|
|
2
|
+
precision highp int;
|
|
3
|
+
|
|
4
|
+
#include <utils>
|
|
5
|
+
|
|
6
|
+
uniform float underwater;
|
|
7
|
+
uniform samplerCube sky;
|
|
8
|
+
|
|
9
|
+
varying vec3 eye;
|
|
10
|
+
varying vec3 pos;
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
vec3 getSurfaceRayColor(vec3 origin, vec3 ray, vec3 waterColor) {
|
|
14
|
+
vec3 color;
|
|
15
|
+
|
|
16
|
+
if (ray.y < 0.0) {
|
|
17
|
+
vec2 t = intersectCube(origin, ray, vec3(-1.0, -poolHeight, -1.0), vec3(1.0, 2.0, 1.0));
|
|
18
|
+
color = getWallColor(origin + ray * t.y);
|
|
19
|
+
} else {
|
|
20
|
+
vec2 t = intersectCube(origin, ray, vec3(-1.0, -poolHeight, -1.0), vec3(1.0, 2.0, 1.0));
|
|
21
|
+
vec3 hit = origin + ray * t.y;
|
|
22
|
+
if (hit.y < 7.0 / 12.0) {
|
|
23
|
+
color = getWallColor(hit);
|
|
24
|
+
} else {
|
|
25
|
+
color = textureCube(sky, ray).rgb;
|
|
26
|
+
color += 0.01 * vec3(pow(max(0.0, dot(light, ray)), 20.0)) * vec3(10.0, 8.0, 6.0);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (ray.y < 0.0) color *= waterColor;
|
|
31
|
+
|
|
32
|
+
return color;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
void main() {
|
|
37
|
+
vec2 coord = pos.xz * 0.5 + 0.5;
|
|
38
|
+
vec4 info = texture2D(water, coord);
|
|
39
|
+
|
|
40
|
+
/* make water look more "peaked" */
|
|
41
|
+
for (int i = 0; i < 5; i++) {
|
|
42
|
+
coord += info.ba * 0.005;
|
|
43
|
+
info = texture2D(water, coord);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
vec3 normal = vec3(info.b, sqrt(1.0 - dot(info.ba, info.ba)), info.a);
|
|
47
|
+
vec3 incomingRay = normalize(pos - eye);
|
|
48
|
+
|
|
49
|
+
if (underwater == 1.) {
|
|
50
|
+
normal = -normal;
|
|
51
|
+
vec3 reflectedRay = reflect(incomingRay, normal);
|
|
52
|
+
vec3 refractedRay = refract(incomingRay, normal, IOR_WATER / IOR_AIR);
|
|
53
|
+
float fresnel = mix(0.5, 1.0, pow(1.0 - dot(normal, -incomingRay), 3.0));
|
|
54
|
+
|
|
55
|
+
vec3 reflectedColor = getSurfaceRayColor(pos, reflectedRay, underwaterColor);
|
|
56
|
+
vec3 refractedColor = getSurfaceRayColor(pos, refractedRay, vec3(1.0)) * vec3(0.8, 1.0, 1.1);
|
|
57
|
+
|
|
58
|
+
gl_FragColor = vec4(mix(reflectedColor, refractedColor, (1.0 - fresnel) * length(refractedRay)), 1.0);
|
|
59
|
+
} else {
|
|
60
|
+
vec3 reflectedRay = reflect(incomingRay, normal);
|
|
61
|
+
vec3 refractedRay = refract(incomingRay, normal, IOR_AIR / IOR_WATER);
|
|
62
|
+
float fresnel = mix(0.25, 1.0, pow(1.0 - dot(normal, -incomingRay), 3.0));
|
|
63
|
+
|
|
64
|
+
vec3 reflectedColor = getSurfaceRayColor(pos, reflectedRay, abovewaterColor);
|
|
65
|
+
vec3 refractedColor = getSurfaceRayColor(pos, refractedRay, abovewaterColor);
|
|
66
|
+
|
|
67
|
+
gl_FragColor = vec4(mix(refractedColor, reflectedColor, fresnel), 1.0);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
uniform mat4 projectionMatrix;
|
|
2
|
+
uniform mat4 modelViewMatrix;
|
|
3
|
+
uniform sampler2D water;
|
|
4
|
+
|
|
5
|
+
attribute vec3 position;
|
|
6
|
+
|
|
7
|
+
varying vec3 eye;
|
|
8
|
+
varying vec3 pos;
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
void main() {
|
|
12
|
+
vec4 info = texture2D(water, position.xy * 0.5 + 0.5);
|
|
13
|
+
pos = position.xzy;
|
|
14
|
+
pos.y += info.r;
|
|
15
|
+
|
|
16
|
+
vec3 axis_x = vec3(modelViewMatrix[0].x, modelViewMatrix[0].y, modelViewMatrix[0].z);
|
|
17
|
+
vec3 axis_y = vec3(modelViewMatrix[1].x, modelViewMatrix[1].y, modelViewMatrix[1].z);
|
|
18
|
+
vec3 axis_z = vec3(modelViewMatrix[2].x, modelViewMatrix[2].y, modelViewMatrix[2].z);
|
|
19
|
+
vec3 offset = vec3(modelViewMatrix[3].x, modelViewMatrix[3].y, modelViewMatrix[3].z);
|
|
20
|
+
|
|
21
|
+
eye = vec3(dot(-offset, axis_x), dot(-offset, axis_y), dot(-offset, axis_z));
|
|
22
|
+
|
|
23
|
+
gl_Position = projectionMatrix * modelViewMatrix * vec4(pos, 1.0);
|
|
24
|
+
}
|
|
Binary file
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "easy-threesdk",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.1",
|
|
4
4
|
"description": "����Threejs��װ�ij��ù��ܵ�sdk",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"private": false,
|
|
7
7
|
"scripts": {
|
|
8
8
|
"test": "echo \"Error: no test specified\" && exit 1"
|
|
9
9
|
},
|
|
10
|
+
"repository": {
|
|
11
|
+
"type": "git",
|
|
12
|
+
"url": "https://gitee.com/xieqianstudent/visual-learning.git"
|
|
13
|
+
},
|
|
10
14
|
"keywords": [
|
|
11
15
|
"Threejs"
|
|
12
16
|
],
|
package/plugins/LabelControl.js
CHANGED
|
@@ -44,7 +44,128 @@ export default class LabelControl {
|
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
/**
|
|
47
|
-
|
|
47
|
+
* 创建3D 指示器背景标签并放在指定的模型上面进行标注
|
|
48
|
+
* @param {string} LabelID - 标签ID
|
|
49
|
+
* @param {string} contentHtml - 标签显示的HTML内容
|
|
50
|
+
* @param {THREE.Vector3|Array} position - 标签的3D位置,可以是Vector3对象或[x, y, z]数组
|
|
51
|
+
* @param {Object} options - 可选配置项
|
|
52
|
+
* @param {number} options.size - 标签的缩放大小,默认0.02
|
|
53
|
+
* @param {string} options.backgroundColor - 背景颜色,默认rgba(20, 30, 50, 0.9)
|
|
54
|
+
* @param {string} options.textColor - 文字颜色,默认#ffffff
|
|
55
|
+
* @param {string} options.borderColor - 边框颜色,默认#22d3ee
|
|
56
|
+
* @param {number} options.fontSize - 字体大小,默认16px
|
|
57
|
+
* @param {number} options.width - 标签宽度,默认auto
|
|
58
|
+
* @param {string} options.className - 自定义CSS类名
|
|
59
|
+
* @returns {CSS3DObject} 返回创建的3D标签对象,可以用于后续操作(如隐藏、删除等)
|
|
60
|
+
*/
|
|
61
|
+
createIndicator3DLabel(LabelID, contentHtml, position, options = {}, Events = {}) {
|
|
62
|
+
// 如果没有场景或标签渲染器,提示错误
|
|
63
|
+
if (!this.ThreeSdk.scene || !this.labelRenderer) {
|
|
64
|
+
console.error("❌ 场景未初始化,请确保 initMap() 已执行");
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// 设置默认选项
|
|
69
|
+
const {
|
|
70
|
+
size = 0.02,
|
|
71
|
+
backgroundColor = "rgba(20, 30, 50, 0.9)",
|
|
72
|
+
textColor = "#ffffff",
|
|
73
|
+
borderColor = "#22d3ee",
|
|
74
|
+
fontSize = 16,
|
|
75
|
+
width = "auto",
|
|
76
|
+
className = "label-3d-Indicator",
|
|
77
|
+
} = options;
|
|
78
|
+
|
|
79
|
+
// 创建HTML元素
|
|
80
|
+
const labelDiv = document.createElement("div");
|
|
81
|
+
labelDiv.className = className;
|
|
82
|
+
|
|
83
|
+
// 设置样式(添加鼠标悬停效果和点击样式)
|
|
84
|
+
labelDiv.style.cssText = `
|
|
85
|
+
position:relative;
|
|
86
|
+
z-index:99;
|
|
87
|
+
pointer-events:none;
|
|
88
|
+
|
|
89
|
+
`;
|
|
90
|
+
const labelIndicatorDiv = document.createElement("div")
|
|
91
|
+
labelDiv.appendChild(labelIndicatorDiv);
|
|
92
|
+
labelIndicatorDiv.style.cssText = `
|
|
93
|
+
display: block;
|
|
94
|
+
background-image: url("/ponit-ui-item.png");
|
|
95
|
+
background-size: 100% 100%;
|
|
96
|
+
width: 313px;
|
|
97
|
+
height: 188px;
|
|
98
|
+
left: -313px;
|
|
99
|
+
position: relative;
|
|
100
|
+
`
|
|
101
|
+
|
|
102
|
+
const labelContentDiv = document.createElement("div")
|
|
103
|
+
labelDiv.appendChild(labelContentDiv);
|
|
104
|
+
labelContentDiv.style.cssText = `
|
|
105
|
+
position: absolute;
|
|
106
|
+
left: -285px;
|
|
107
|
+
top: 55px;
|
|
108
|
+
background: #000515;
|
|
109
|
+
border-radius: 9px;
|
|
110
|
+
padding: 4px;
|
|
111
|
+
/* box-shadow: 0px 0px 10px #03f4fe; */
|
|
112
|
+
border: 2px solid #02d9e6;
|
|
113
|
+
`
|
|
114
|
+
labelContentDiv.innerHTML = contentHtml;
|
|
115
|
+
|
|
116
|
+
// 添加鼠标悬停效果
|
|
117
|
+
labelContentDiv.addEventListener("mouseenter", () => {
|
|
118
|
+
labelContentDiv.style.boxShadow = `0 6px 16px rgba(34, 211, 238, 0.5)`;
|
|
119
|
+
labelContentDiv.style.borderColor = "#60e5ff";
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
labelContentDiv.addEventListener("mouseleave", () => {
|
|
123
|
+
labelContentDiv.style.boxShadow = `0 4px 12px rgba(34, 211, 238, 0.3)`;
|
|
124
|
+
labelContentDiv.style.borderColor = borderColor;
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
// 创建 CSS3DObject
|
|
131
|
+
const label = new CSS3DObject(labelDiv);
|
|
132
|
+
|
|
133
|
+
// 设置位置
|
|
134
|
+
if (position instanceof THREE.Vector3) {
|
|
135
|
+
label.position.copy(position);
|
|
136
|
+
} else if (Array.isArray(position) && position.length >= 3) {
|
|
137
|
+
label.position.set(position[0], position[1], position[2]);
|
|
138
|
+
} else {
|
|
139
|
+
console.error("❌ 位置参数无效,请使用 THREE.Vector3 或 [x, y, z] 数组");
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// 设置缩放(CSS3D默认很小,需要放大)
|
|
144
|
+
label.scale.set(size, size, size);
|
|
145
|
+
this.bindEvents(labelDiv, label, Events);
|
|
146
|
+
|
|
147
|
+
// 添加点击事件:点击标签后,相机移动到标签下方
|
|
148
|
+
// labelDiv.addEventListener("click", (e) => {
|
|
149
|
+
// e.stopPropagation(); // 阻止事件冒泡
|
|
150
|
+
// click(e);
|
|
151
|
+
// // this.moveCameraToLabel(label.position, {
|
|
152
|
+
// // distance: 0,
|
|
153
|
+
// // heightOffset: 20,
|
|
154
|
+
// // });
|
|
155
|
+
// });
|
|
156
|
+
|
|
157
|
+
console.log(
|
|
158
|
+
`✅ 3D标签已创建: "${contentHtml}" 位置: (${label.position.x}, ${label.position.y}, ${label.position.z})`
|
|
159
|
+
);
|
|
160
|
+
|
|
161
|
+
this._LabelMap.set(LabelID, label);
|
|
162
|
+
this.ThreeSdk.scene.add(label);
|
|
163
|
+
// 返回标签对象,方便后续操作
|
|
164
|
+
return label;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* 创建3D 箭头标签并放在指定的模型上面进行标注
|
|
48
169
|
* @param {string} LabelID - 标签ID
|
|
49
170
|
* @param {string} contentHtml - 标签显示的HTML内容
|
|
50
171
|
* @param {THREE.Vector3|Array} position - 标签的3D位置,可以是Vector3对象或[x, y, z]数组
|
|
@@ -58,7 +179,7 @@ export default class LabelControl {
|
|
|
58
179
|
* @param {string} options.className - 自定义CSS类名
|
|
59
180
|
* @returns {CSS3DObject} 返回创建的3D标签对象,可以用于后续操作(如隐藏、删除等)
|
|
60
181
|
*/
|
|
61
|
-
|
|
182
|
+
createArrow3DLabel(LabelID, contentHtml, position, options = {}, Events = {}) {
|
|
62
183
|
// 如果没有场景或标签渲染器,提示错误
|
|
63
184
|
if (!this.ThreeSdk.scene || !this.labelRenderer) {
|
|
64
185
|
console.error("❌ 场景未初始化,请确保 initMap() 已执行");
|
|
@@ -73,7 +194,7 @@ export default class LabelControl {
|
|
|
73
194
|
borderColor = "#22d3ee",
|
|
74
195
|
fontSize = 16,
|
|
75
196
|
width = "auto",
|
|
76
|
-
className = "label-3d-
|
|
197
|
+
className = "label-3d-arrow",
|
|
77
198
|
} = options;
|
|
78
199
|
|
|
79
200
|
// 创建HTML元素
|
|
@@ -168,11 +289,167 @@ export default class LabelControl {
|
|
|
168
289
|
return label;
|
|
169
290
|
}
|
|
170
291
|
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* 创建3D 可展开标签并放在指定的模型上面进行标注
|
|
295
|
+
* @param {string} LabelID - 标签ID
|
|
296
|
+
* @param {string} contentHtml - 标签显示的HTML内容
|
|
297
|
+
* @param {THREE.Vector3|Array} position - 标签的3D位置,可以是Vector3对象或[x, y, z]数组
|
|
298
|
+
* @param {Object} options - 可选配置项
|
|
299
|
+
* @param {number} options.size - 标签的缩放大小,默认0.02
|
|
300
|
+
* @param {string} options.backgroundColor - 背景颜色,默认rgba(20, 30, 50, 0.9)
|
|
301
|
+
* @param {string} options.textColor - 文字颜色,默认#ffffff
|
|
302
|
+
* @param {string} options.borderColor - 边框颜色,默认#22d3ee
|
|
303
|
+
* @param {number} options.fontSize - 字体大小,默认16px
|
|
304
|
+
* @param {number} options.width - 标签宽度,默认auto
|
|
305
|
+
* @param {string} options.className - 自定义CSS类名
|
|
306
|
+
* @returns {CSS3DObject} 返回创建的3D标签对象,可以用于后续操作(如隐藏、删除等)
|
|
307
|
+
*/
|
|
308
|
+
createExpand3DLabel(LabelID, contentHtml, expandContentHtml, position, options = {}, Events = {}) {
|
|
309
|
+
// 如果没有场景或标签渲染器,提示错误
|
|
310
|
+
if (!this.ThreeSdk.scene || !this.labelRenderer) {
|
|
311
|
+
console.error("❌ 场景未初始化,请确保 initMap() 已执行");
|
|
312
|
+
return null;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// 设置默认选项
|
|
316
|
+
const {
|
|
317
|
+
size = 0.02,
|
|
318
|
+
backgroundColor = "rgba(20, 30, 50, 0.9)",
|
|
319
|
+
textColor = "#ffffff",
|
|
320
|
+
borderColor = "#22d3ee",
|
|
321
|
+
fontSize = 16,
|
|
322
|
+
width = "auto",
|
|
323
|
+
className = "label-3d-Expand",
|
|
324
|
+
} = options;
|
|
325
|
+
|
|
326
|
+
// 创建HTML元素
|
|
327
|
+
const labelDiv = document.createElement("div");
|
|
328
|
+
labelDiv.className = className;
|
|
329
|
+
|
|
330
|
+
// 设置样式(添加鼠标悬停效果和点击样式)
|
|
331
|
+
labelDiv.style.cssText = `
|
|
332
|
+
position: relative;
|
|
333
|
+
height: 50px;
|
|
334
|
+
color: #fff;
|
|
335
|
+
border-radius: 50px;
|
|
336
|
+
background-color: rgba(0, 0, 0, 0.3);
|
|
337
|
+
display: table-cell;
|
|
338
|
+
line-height: 50px;
|
|
339
|
+
/* vertical-align: unset; */
|
|
340
|
+
vertical-align: middle;
|
|
341
|
+
pointer-events: auto; /* 允许标签接收鼠标事件 */
|
|
342
|
+
user-select: none;
|
|
343
|
+
background: rgba(255, 255, 255, .4);
|
|
344
|
+
-webkit-backdrop-filter: blur(5px);
|
|
345
|
+
backdrop-filter: blur(5px);
|
|
346
|
+
border:1px solid white;
|
|
347
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Microsoft YaHei', sans-serif;
|
|
348
|
+
cursor: pointer; /* 鼠标悬停时显示手型光标 */
|
|
349
|
+
transition: all 0.3s ease; /* 添加过渡动画 */
|
|
350
|
+
`;
|
|
351
|
+
|
|
352
|
+
// 添加鼠标悬停效果
|
|
353
|
+
labelDiv.addEventListener("mouseenter", () => {
|
|
354
|
+
labelDiv.style.boxShadow = `0 6px 16px rgba(34, 211, 238, 0.5)`;
|
|
355
|
+
labelDiv.style.borderColor = "#60e5ff";
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
labelDiv.addEventListener("mouseleave", () => {
|
|
359
|
+
labelDiv.style.boxShadow = `0 4px 12px rgba(34, 211, 238, 0.3)`;
|
|
360
|
+
labelDiv.style.borderColor = borderColor;
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
// 设置标签文本内容
|
|
364
|
+
labelDiv.innerHTML = contentHtml;
|
|
365
|
+
|
|
366
|
+
//展开内容
|
|
367
|
+
const expandDiv = document.createElement("div")
|
|
368
|
+
labelDiv.appendChild(expandDiv)
|
|
369
|
+
expandDiv.style.cssText = `
|
|
370
|
+
position:absolute;
|
|
371
|
+
background: rgba(255, 255, 255, .4);
|
|
372
|
+
-webkit-backdrop-filter: blur(5px);
|
|
373
|
+
backdrop-filter: blur(5px);
|
|
374
|
+
border:1px solid white;
|
|
375
|
+
border-radius: 10px;
|
|
376
|
+
display: none;
|
|
377
|
+
top:0px;
|
|
378
|
+
`
|
|
379
|
+
const expandCloseBtn = document.createElement("div")
|
|
380
|
+
expandCloseBtn.innerHTML = "关闭"
|
|
381
|
+
expandDiv.appendChild(expandCloseBtn)
|
|
382
|
+
expandCloseBtn.style.cssText = `
|
|
383
|
+
display:flex;
|
|
384
|
+
align-items: center;
|
|
385
|
+
justify-content: flex-end;
|
|
386
|
+
cursor:pointer;
|
|
387
|
+
height:30px;
|
|
388
|
+
padding:0px 10px;
|
|
389
|
+
`
|
|
390
|
+
expandCloseBtn.addEventListener("click", (e) => {
|
|
391
|
+
e.stopPropagation()
|
|
392
|
+
expandDiv.style.display = "none"
|
|
393
|
+
})
|
|
394
|
+
|
|
395
|
+
const expandContentDiv = document.createElement("div")
|
|
396
|
+
expandDiv.appendChild(expandContentDiv)
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
expandContentDiv.innerHTML = expandContentHtml
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
labelDiv.addEventListener("click", () => {
|
|
404
|
+
|
|
405
|
+
expandDiv.style.display = "block"
|
|
406
|
+
expandDiv.style.right = (-expandDiv.clientWidth - 20) + "px" //将展开内容定位到标签右边
|
|
407
|
+
expandDiv.style.top = (-expandDiv.clientHeight / 2) + "px"
|
|
408
|
+
})
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
// 创建 CSS3DObject
|
|
412
|
+
const label = new CSS3DObject(labelDiv);
|
|
413
|
+
|
|
414
|
+
// 设置位置
|
|
415
|
+
if (position instanceof THREE.Vector3) {
|
|
416
|
+
label.position.copy(position);
|
|
417
|
+
} else if (Array.isArray(position) && position.length >= 3) {
|
|
418
|
+
label.position.set(position[0], position[1], position[2]);
|
|
419
|
+
} else {
|
|
420
|
+
console.error("❌ 位置参数无效,请使用 THREE.Vector3 或 [x, y, z] 数组");
|
|
421
|
+
return null;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// 设置缩放(CSS3D默认很小,需要放大)
|
|
425
|
+
label.scale.set(size, size, size);
|
|
426
|
+
this.bindEvents(labelDiv, label, Events);
|
|
427
|
+
|
|
428
|
+
// 添加点击事件:点击标签后,相机移动到标签下方
|
|
429
|
+
// labelDiv.addEventListener("click", (e) => {
|
|
430
|
+
// e.stopPropagation(); // 阻止事件冒泡
|
|
431
|
+
// click(e);
|
|
432
|
+
// // this.moveCameraToLabel(label.position, {
|
|
433
|
+
// // distance: 0,
|
|
434
|
+
// // heightOffset: 20,
|
|
435
|
+
// // });
|
|
436
|
+
// });
|
|
437
|
+
|
|
438
|
+
console.log(
|
|
439
|
+
`✅ 3D标签已创建: "${contentHtml}" 位置: (${label.position.x}, ${label.position.y}, ${label.position.z})`
|
|
440
|
+
);
|
|
441
|
+
|
|
442
|
+
this._LabelMap.set(LabelID, label);
|
|
443
|
+
this.ThreeSdk.scene.add(label);
|
|
444
|
+
// 返回标签对象,方便后续操作
|
|
445
|
+
return label;
|
|
446
|
+
}
|
|
447
|
+
|
|
171
448
|
bindEvents(labelDiv, label, Events = {}) {
|
|
172
449
|
const {
|
|
173
|
-
click = () => {},
|
|
174
|
-
mouseenter = () => {},
|
|
175
|
-
mouseleave = () => {},
|
|
450
|
+
click = () => { },
|
|
451
|
+
mouseenter = () => { },
|
|
452
|
+
mouseleave = () => { },
|
|
176
453
|
} = Events;
|
|
177
454
|
labelDiv.addEventListener("click", (e) => {
|
|
178
455
|
e.stopPropagation(); // 阻止事件冒泡
|
package/plugins/ModelControl.js
CHANGED
|
@@ -18,7 +18,7 @@ export default class ModelControl {
|
|
|
18
18
|
this.ThreeSdk = ThreeSdk;
|
|
19
19
|
this._init();
|
|
20
20
|
}
|
|
21
|
-
_init() {}
|
|
21
|
+
_init() { }
|
|
22
22
|
|
|
23
23
|
/**
|
|
24
24
|
* 加载 GLTF 格式的建筑物模型
|
|
@@ -66,12 +66,7 @@ export default class ModelControl {
|
|
|
66
66
|
if (clickable) {
|
|
67
67
|
this._clickableObjects.push(child);
|
|
68
68
|
}
|
|
69
|
-
|
|
70
|
-
if (child.name === "Geom3D_4330") {
|
|
71
|
-
console.log("✅ 找到水阀开关: Geom3D_4330");
|
|
72
|
-
// 保存初始旋转状态
|
|
73
|
-
child.userData.initialRotation = child.rotation.clone();
|
|
74
|
-
}
|
|
69
|
+
|
|
75
70
|
|
|
76
71
|
// 打印网格信息
|
|
77
72
|
console.log(`GLTF 网格: "${child.name}"`);
|
|
@@ -102,9 +97,11 @@ export default class ModelControl {
|
|
|
102
97
|
// 将模型居中:使模型的中心位于场景中心 (0, 0, 0)
|
|
103
98
|
// position[0] 和 position[1] 是用户想要模型中心的位置
|
|
104
99
|
// 但由于模型本身有中心偏移,需要减去 centerScaled 来补偿
|
|
105
|
-
model.position.x = position[0]
|
|
100
|
+
// model.position.x = position[0]- centerScaled.x; // 使模型中心在 position[0]
|
|
101
|
+
model.position.x = position[0]
|
|
106
102
|
model.position.y = -minY * scaleFactor; // 放置在地面上
|
|
107
|
-
model.position.z = position[1]
|
|
103
|
+
model.position.z = position[1]
|
|
104
|
+
// model.position.z = position[1]- centerScaled.z; // 使模型中心在 position[1]
|
|
108
105
|
|
|
109
106
|
// 相机始终看向场景中心 (0, 0, 0),而不是模型位置
|
|
110
107
|
// 这样地形(点阵底座)和模型都会在视野中,地形不会偏移
|
|
@@ -147,7 +144,7 @@ export default class ModelControl {
|
|
|
147
144
|
}
|
|
148
145
|
|
|
149
146
|
//开启模型鼠标移动事件监听 - 用于悬停效果
|
|
150
|
-
onModelMouseMoveListener(callback = () => {}) {
|
|
147
|
+
onModelMouseMoveListener(callback = () => { }) {
|
|
151
148
|
window.addEventListener("mousemove", (event) => {
|
|
152
149
|
let mouse = new THREE.Vector2();
|
|
153
150
|
// 计算鼠标在标准化设备坐标中的位置 (-1 到 +1)
|
|
@@ -220,7 +217,7 @@ export default class ModelControl {
|
|
|
220
217
|
}
|
|
221
218
|
|
|
222
219
|
//开启模型鼠标点击事件监听
|
|
223
|
-
onModelClickListener(callback = () => {}) {
|
|
220
|
+
onModelClickListener(callback = () => { }) {
|
|
224
221
|
window.addEventListener("click", (event) => {
|
|
225
222
|
let mouse = new THREE.Vector2();
|
|
226
223
|
// 计算鼠标位置
|