MikaFil commited on
Commit
2f3089c
·
verified ·
1 Parent(s): 3bb2289

Update viewer.js

Browse files
Files changed (1) hide show
  1. viewer.js +275 -17
viewer.js CHANGED
@@ -1,4 +1,5 @@
1
- // viewer.js (updated with explicit KHR GLB extension support)
 
2
 
3
  let pc;
4
  export let app = null;
@@ -9,6 +10,7 @@ let bouchonEntity = null;
9
  let viewerInitialized = false;
10
  let resizeObserver = null;
11
 
 
12
  let glossyRedMat = null;
13
 
14
  let chosenCameraX, chosenCameraY, chosenCameraZ;
@@ -57,7 +59,10 @@ export async function initializeViewer(config, instanceId) {
57
  chosenCameraZ = isMobile ? cameraZPhone : cameraZ;
58
 
59
  const canvasId = 'canvas-' + instanceId;
 
 
60
  const viewerContainer = document.getElementById('viewer-container-' + instanceId);
 
61
  let oldCanvas = document.getElementById(canvasId);
62
  if (oldCanvas) oldCanvas.remove();
63
 
@@ -66,13 +71,35 @@ export async function initializeViewer(config, instanceId) {
66
  canvas.className = 'ply-canvas';
67
  canvas.style.width = "100%";
68
  canvas.style.height = "100%";
69
- viewerContainer.appendChild(canvas);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
70
 
71
  if (!pc) {
72
  pc = await import("https://esm.run/playcanvas");
73
  window.pc = pc;
74
  }
75
 
 
76
  const device = await pc.createGraphicsDevice(canvas, {
77
  deviceTypes: ["webgl2"],
78
  glslangUrl: "https://playcanvas.vercel.app/static/lib/glslang/glslang.js",
@@ -83,47 +110,278 @@ export async function initializeViewer(config, instanceId) {
83
 
84
  const opts = new pc.AppOptions();
85
  opts.graphicsDevice = device;
 
86
  opts.mouse = new pc.Mouse(canvas);
87
  opts.touch = new pc.TouchDevice(canvas);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
88
 
89
  app = new pc.Application(canvas, opts);
90
  app.setCanvasFillMode(pc.FILLMODE_NONE);
91
  app.setCanvasResolution(pc.RESOLUTION_AUTO);
92
 
93
- // Explicitly enable KHR GLB extensions
94
- app.loader.getHandler('container').pcMaterialOptions = {
95
- processTransmission: true,
96
- processVolume: true,
97
- processIOR: true,
98
- processSheen: true,
99
- processClearCoat: true,
100
- processVariants: true
101
- };
 
 
 
 
 
102
 
103
- resizeObserver = new ResizeObserver(entries => entries.forEach(entry => app.resizeCanvas(entry.contentRect.width, entry.contentRect.height)));
 
 
 
 
104
  resizeObserver.observe(viewerContainer);
105
 
 
 
 
 
106
  const assets = {
 
 
 
107
  bouchon: new pc.Asset('glb', 'container', {url: glbUrl3}),
 
 
 
 
 
 
 
 
 
108
  };
109
  for (const key in assets) app.assets.add(assets[key]);
110
 
111
  const loader = new pc.AssetListLoader(Object.values(assets), app.assets);
112
  loader.load(() => {
113
  app.start();
 
 
 
 
 
 
 
 
114
 
115
  bouchonEntity = assets.bouchon.resource.instantiateRenderEntity();
116
- bouchonEntity.setLocalScale(modelScale, modelScale, modelScale);
117
- bouchonEntity.setPosition(modelX, modelY, modelZ);
118
- bouchonEntity.setLocalEulerAngles(modelRotationX, modelRotationY, modelRotationZ);
119
  app.root.addChild(bouchonEntity);
120
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
121
  cameraEntity = new pc.Entity('camera');
122
- cameraEntity.addComponent('camera');
123
  cameraEntity.setPosition(chosenCameraX, chosenCameraY, chosenCameraZ);
124
- cameraEntity.lookAt(bouchonEntity.getPosition());
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
125
  app.root.addChild(cameraEntity);
126
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
127
  viewerInitialized = true;
128
  });
129
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // viewer.js
2
+ // ==============================
3
 
4
  let pc;
5
  export let app = null;
 
10
  let viewerInitialized = false;
11
  let resizeObserver = null;
12
 
13
+ // These will hold references to the material(s) you want to update.
14
  let glossyRedMat = null;
15
 
16
  let chosenCameraX, chosenCameraY, chosenCameraZ;
 
59
  chosenCameraZ = isMobile ? cameraZPhone : cameraZ;
60
 
61
  const canvasId = 'canvas-' + instanceId;
62
+ const progressDialog = document.getElementById('progress-dialog-' + instanceId);
63
+ const progressIndicator = document.getElementById('progress-indicator-' + instanceId);
64
  const viewerContainer = document.getElementById('viewer-container-' + instanceId);
65
+
66
  let oldCanvas = document.getElementById(canvasId);
67
  if (oldCanvas) oldCanvas.remove();
68
 
 
71
  canvas.className = 'ply-canvas';
72
  canvas.style.width = "100%";
73
  canvas.style.height = "100%";
74
+ canvas.setAttribute('tabindex', '0');
75
+ // Safer insert:
76
+ if (progressDialog) {
77
+ viewerContainer.insertBefore(canvas, progressDialog);
78
+ } else {
79
+ viewerContainer.appendChild(canvas);
80
+ }
81
+
82
+ canvas.style.touchAction = "none";
83
+ canvas.style.webkitTouchCallout = "none";
84
+ canvas.addEventListener('gesturestart', e => e.preventDefault());
85
+ canvas.addEventListener('gesturechange', e => e.preventDefault());
86
+ canvas.addEventListener('gestureend', e => e.preventDefault());
87
+ canvas.addEventListener('dblclick', e => e.preventDefault());
88
+ canvas.addEventListener('touchstart', e => { if (e.touches.length > 1) e.preventDefault(); }, { passive: false });
89
+
90
+ // --- The following line attaches mouse wheel suppression to canvas only ---
91
+ canvas.addEventListener('wheel', (e) => {
92
+ e.preventDefault(); // Only block page scroll if mouse is over viewer
93
+ }, { passive: false });
94
+
95
+ if (progressDialog) progressDialog.style.display = 'block';
96
 
97
  if (!pc) {
98
  pc = await import("https://esm.run/playcanvas");
99
  window.pc = pc;
100
  }
101
 
102
+ // Create app first
103
  const device = await pc.createGraphicsDevice(canvas, {
104
  deviceTypes: ["webgl2"],
105
  glslangUrl: "https://playcanvas.vercel.app/static/lib/glslang/glslang.js",
 
110
 
111
  const opts = new pc.AppOptions();
112
  opts.graphicsDevice = device;
113
+ // Attach input only to canvas
114
  opts.mouse = new pc.Mouse(canvas);
115
  opts.touch = new pc.TouchDevice(canvas);
116
+ opts.componentSystems = [
117
+ pc.RenderComponentSystem,
118
+ pc.CameraComponentSystem,
119
+ pc.LightComponentSystem,
120
+ pc.ScriptComponentSystem,
121
+ pc.GSplatComponentSystem,
122
+ pc.CollisionComponentSystem,
123
+ pc.RigidbodyComponentSystem
124
+ ];
125
+ opts.resourceHandlers = [
126
+ pc.TextureHandler,
127
+ pc.ContainerHandler,
128
+ pc.ScriptHandler,
129
+ pc.GSplatHandler
130
+ ];
131
 
132
  app = new pc.Application(canvas, opts);
133
  app.setCanvasFillMode(pc.FILLMODE_NONE);
134
  app.setCanvasResolution(pc.RESOLUTION_AUTO);
135
 
136
+ // ******** KHR GLB EXTENSIONS SUPPORT (inserted here) ********
137
+ // This ensures KHR_materials_transmission, KHR_materials_volume, KHR_materials_ior, etc. are handled!
138
+ const containerHandler = app.loader.getHandler('container');
139
+ if (containerHandler) {
140
+ containerHandler.pcMaterialOptions = {
141
+ processTransmission: true,
142
+ processVolume: true,
143
+ processIOR: true,
144
+ processSheen: true,
145
+ processClearCoat: true,
146
+ processVariants: true
147
+ };
148
+ }
149
+ // ***********************************************************
150
 
151
+ resizeObserver = new ResizeObserver(entries => {
152
+ entries.forEach(entry => {
153
+ app.resizeCanvas(entry.contentRect.width, entry.contentRect.height);
154
+ });
155
+ });
156
  resizeObserver.observe(viewerContainer);
157
 
158
+ window.addEventListener('resize', () => app.resizeCanvas(viewerContainer.clientWidth, viewerContainer.clientHeight));
159
+ app.on('destroy', () => resizeObserver.disconnect());
160
+
161
+ // Assets after app exists
162
  const assets = {
163
+ orbit: new pc.Asset('script', 'script', { url: "https://mikafil-viewer-gs.static.hf.space/orbit-camera.js" }),
164
+ model: new pc.Asset('glb', 'container', { url: glbUrl }),
165
+ tube: new pc.Asset('glb', 'container', {url: glbUrl2}),
166
  bouchon: new pc.Asset('glb', 'container', {url: glbUrl3}),
167
+ ao_map: new pc.Asset('color', 'texture', {url: aoUrl}),
168
+ op_map: new pc.Asset('color', 'texture', {url: opacityUrl}),
169
+ thickness_map: new pc.Asset('color', 'texture', {url: thicknessUrl}),
170
+ helipad: new pc.Asset(
171
+ 'helipad-env-atlas',
172
+ 'texture',
173
+ { url: "https://huggingface.co/datasets/MikaFil/3D_models/resolve/main/EARCARE/_rsc/hdr/citrus_orchard_road_puresky_1k.png" },
174
+ { type: pc.TEXTURETYPE_RGBP, mipmaps: false }
175
+ )
176
  };
177
  for (const key in assets) app.assets.add(assets[key]);
178
 
179
  const loader = new pc.AssetListLoader(Object.values(assets), app.assets);
180
  loader.load(() => {
181
  app.start();
182
+ if (progressDialog) progressDialog.style.display = 'none';
183
+
184
+ // --- Add the GLB entity ---
185
+ modelEntity = assets.model.resource.instantiateRenderEntity();
186
+ app.root.addChild(modelEntity);
187
+
188
+ tubeEntity = assets.tube.resource.instantiateRenderEntity();
189
+ app.root.addChild(tubeEntity);
190
 
191
  bouchonEntity = assets.bouchon.resource.instantiateRenderEntity();
 
 
 
192
  app.root.addChild(bouchonEntity);
193
 
194
+ // --- Add a light ---
195
+ const light = new pc.Entity("mainLight");
196
+ light.addComponent('light',{
197
+ type: "directional",
198
+ color: new pc.Color(1, 1, 1),
199
+ intensity:0.9,
200
+ });
201
+ light.setPosition(0, 1, -1);
202
+ light.lookAt(0, 0, 0);
203
+ app.root.addChild(light);
204
+
205
+ // --- Glossy Red Material (exposed globally for changeColor)
206
+ glossyRedMat = new pc.StandardMaterial();
207
+ const ao = assets.ao_map.resource;
208
+ const op = assets.op_map.resource;
209
+ const thickness = assets.thickness_map.resource;
210
+ glossyRedMat.blendType = pc.BLEND_NORMAL;
211
+ glossyRedMat.diffuse = new pc.Color(0.7, 0.05, 0.05);
212
+ glossyRedMat.specular = new pc.Color(0.01, 0.01, 0.01);
213
+ glossyRedMat.shininess = 90;
214
+ glossyRedMat.gloss = 1;
215
+ glossyRedMat.metalness = 0.0;
216
+ glossyRedMat.useMetalness = true;
217
+ glossyRedMat.useDynamicRefraction = true;
218
+ glossyRedMat.refraction= 0.8;
219
+ glossyRedMat.refractionIndex = 0.5;
220
+ glossyRedMat.thickness = 5;
221
+ glossyRedMat.thicknessMap = thickness;
222
+ glossyRedMat.opacityMap = op;
223
+ glossyRedMat.opacityMapChannel = "r";
224
+ glossyRedMat.opacity = 0.975;
225
+ glossyRedMat.emissive = new pc.Color(0.372, 0.03, 0.003);
226
+ glossyRedMat.emissiveMap = ao;
227
+ glossyRedMat.emissiveIntensity = 1;
228
+ glossyRedMat.update();
229
+
230
+ const tubeEmit = new pc.StandardMaterial();
231
+ tubeEmit.emissive = new pc.Color(0.372, 0.2, 0.2);
232
+ tubeEmit.emissiveIntensity = 5;
233
+ tubeEmit.opacity = 0.99;
234
+
235
+ // RECURSIVE MATERIAL ASSIGNMENT (replace forEach with traverse)
236
+ function traverse(entity, callback) {
237
+ callback(entity);
238
+ entity.children.forEach(child => traverse(child, callback));
239
+ }
240
+ // Assign main model material for color switching
241
+ traverse(modelEntity, node => {
242
+ if (node.render && node.render.meshInstances) {
243
+ for (let mi of node.render.meshInstances) {
244
+ mi.material = glossyRedMat;
245
+ }
246
+ }
247
+ });
248
+ // Tube
249
+ traverse(tubeEntity, node => {
250
+ if (node.render && node.render.meshInstances) {
251
+ for (let mi of node.render.meshInstances) {
252
+ mi.material = tubeEmit;
253
+ }
254
+ }
255
+ });
256
+
257
+ // Set model transform
258
+ modelEntity.setPosition(modelX, modelY, modelZ);
259
+ modelEntity.setLocalScale(modelScale, modelScale, modelScale);
260
+ modelEntity.setLocalEulerAngles(modelRotationX, modelRotationY, modelRotationZ);
261
+
262
+ tubeEntity.setPosition(modelX, modelY, modelZ);
263
+ tubeEntity.setLocalScale(modelScale, modelScale, modelScale);
264
+ tubeEntity.setLocalEulerAngles(modelRotationX, modelRotationY, modelRotationZ);
265
+
266
  cameraEntity = new pc.Entity('camera');
267
+ cameraEntity.addComponent('camera', { clearColor: new pc.Color(0.95, 0.95, 0.95, 1) });
268
  cameraEntity.setPosition(chosenCameraX, chosenCameraY, chosenCameraZ);
269
+ cameraEntity.lookAt(modelEntity.getPosition());
270
+ cameraEntity.addComponent('script');
271
+
272
+ // Setup skydome
273
+ app.scene.envAtlas = assets.helipad.resource;
274
+ app.scene.skyboxRotation = new pc.Quat().setFromEulerAngles(30, 70, 0);
275
+ app.scene.skyboxIntensity = 2;
276
+ app.scene.skyboxMip = 0;
277
+
278
+ // Hide skybox layer so HDR is not rendered as background
279
+ const LAYERID_SKYBOX = app.scene.layers.getLayerByName("Skybox").id;
280
+ //app.scene.layers.getLayerById(LAYERID_SKYBOX).enabled = false;
281
+
282
+ // Pass ALL relevant attributes including minY
283
+ cameraEntity.script.create('orbitCamera', {
284
+ attributes: {
285
+ focusEntity: modelEntity,
286
+ inertiaFactor: 0.2,
287
+ distanceMax: maxZoom,
288
+ distanceMin: minZoom,
289
+ pitchAngleMax: maxAngle,
290
+ pitchAngleMin: minAngle,
291
+ yawAngleMax: maxAzimuth,
292
+ yawAngleMin: minAzimuth,
293
+ minY: minY,
294
+ frameOnStart: false
295
+ }
296
+ });
297
+ cameraEntity.script.create('orbitCameraInputMouse');
298
+ cameraEntity.script.create('orbitCameraInputTouch');
299
  app.root.addChild(cameraEntity);
300
 
301
+ app.resizeCanvas(viewerContainer.clientWidth, viewerContainer.clientHeight);
302
+
303
+ app.once('update', () => resetViewerCamera());
304
+
305
+ // Tooltips supported if tooltips_url set
306
+ try {
307
+ if (config.tooltips_url) {
308
+ import('./tooltips.js').then(tooltipsModule => {
309
+ tooltipsModule.initializeTooltips({
310
+ app,
311
+ cameraEntity,
312
+ modelEntity,
313
+ tooltipsUrl: config.tooltips_url,
314
+ defaultVisible: !!config.showTooltipsDefault,
315
+ moveDuration: config.tooltipMoveDuration || 0.6
316
+ });
317
+ }).catch(e => {
318
+ // Tooltips optional: fail silently if missing
319
+ });
320
+ }
321
+ } catch (e) {
322
+ // Tooltips optional, fail silently
323
+ }
324
+
325
  viewerInitialized = true;
326
  });
327
  }
328
+
329
+ export function resetViewerCamera() {
330
+ try {
331
+ if (!cameraEntity || !modelEntity || !app) return;
332
+ const orbitCam = cameraEntity.script.orbitCamera;
333
+ if (!orbitCam) return;
334
+
335
+ const modelPos = modelEntity.getPosition();
336
+ const tempEnt = new pc.Entity();
337
+ tempEnt.setPosition(chosenCameraX, chosenCameraY, chosenCameraZ);
338
+ tempEnt.lookAt(modelPos);
339
+
340
+ const dist = new pc.Vec3().sub2(
341
+ new pc.Vec3(chosenCameraX, chosenCameraY, chosenCameraZ),
342
+ modelPos
343
+ ).length();
344
+
345
+ cameraEntity.setPosition(chosenCameraX, chosenCameraY, chosenCameraZ);
346
+ cameraEntity.lookAt(modelPos);
347
+
348
+ orbitCam.pivotPoint = modelPos.clone();
349
+ orbitCam._targetDistance = dist;
350
+ orbitCam._distance = dist;
351
+
352
+ const rot = tempEnt.getRotation();
353
+ const fwd = new pc.Vec3();
354
+ rot.transformVector(pc.Vec3.FORWARD, fwd);
355
+
356
+ const yaw = Math.atan2(-fwd.x, -fwd.z) * pc.math.RAD_TO_DEG;
357
+ const yawQuat = new pc.Quat().setFromEulerAngles(0, -yaw, 0);
358
+ const rotNoYaw = new pc.Quat().mul2(yawQuat, rot);
359
+ const fNoYaw = new pc.Vec3();
360
+ rotNoYaw.transformVector(pc.Vec3.FORWARD, fNoYaw);
361
+ const pitch = Math.atan2(fNoYaw.y, -fNoYaw.z) * pc.math.RAD_TO_DEG;
362
+
363
+ orbitCam._targetYaw = yaw;
364
+ orbitCam._yaw = yaw;
365
+ orbitCam._targetPitch = pitch;
366
+ orbitCam._pitch = pitch;
367
+ if (orbitCam._updatePosition) orbitCam._updatePosition();
368
+
369
+ tempEnt.destroy();
370
+ } catch (e) {
371
+ // Silent fail
372
+ }
373
+ }
374
+
375
+ /**
376
+ * Change the main model's diffuse color in real time.
377
+ * r,g,b are floats [0,1]. This is a reusable function for new colors.
378
+ */
379
+ export function changeColor(dr, dg, db, a, er, eg, eb) {
380
+ // Wait until glossyRedMat is loaded (null before model load).
381
+ if (!glossyRedMat) return;
382
+
383
+ glossyRedMat.diffuse.set(dr, dg, db);
384
+ glossyRedMat.emissive.set(er, eg, eb);
385
+ glossyRedMat.opacity = a;
386
+ glossyRedMat.update();
387
+ }