MikaFil commited on
Commit
5652a0a
·
verified ·
1 Parent(s): cc93f09

Update viewer.js

Browse files
Files changed (1) hide show
  1. viewer.js +74 -61
viewer.js CHANGED
@@ -1,19 +1,19 @@
1
- // viewer.js - iOS/Android/Desktop GSplat+GLB viewer, robust drop-in
2
 
3
- let pc;
4
  export let app = null;
5
  let cameraEntity = null;
6
  let modelEntity = null;
7
  let viewerInitialized = false;
8
  let resizeObserver = null;
9
- let loadTimeout = null;
10
 
11
- // Camera/model config
12
  let chosenCameraX, chosenCameraY, chosenCameraZ;
13
  let minZoom, maxZoom, minAngle, maxAngle, minAzimuth, maxAzimuth, minPivotY, minY;
14
  let modelX, modelY, modelZ, modelScale, modelRotationX, modelRotationY, modelRotationZ;
15
  let plyUrl, glbUrl;
16
 
 
17
  function isIOS() {
18
  return /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream;
19
  }
@@ -25,6 +25,7 @@ function logAndAlert(msg) {
25
  export async function initializeViewer(config, instanceId) {
26
  if (viewerInitialized) return;
27
 
 
28
  plyUrl = config.ply_url;
29
  glbUrl = config.glb_url;
30
  minZoom = parseFloat(config.minZoom || "1");
@@ -56,7 +57,7 @@ export async function initializeViewer(config, instanceId) {
56
  chosenCameraY = mobile ? cameraYPhone : cameraY;
57
  chosenCameraZ = mobile ? cameraZPhone : cameraZ;
58
 
59
- // DOM setup
60
  const canvasId = 'canvas-' + instanceId;
61
  const progressDialog = document.getElementById('progress-dialog-' + instanceId);
62
  const progressIndicator = document.getElementById('progress-indicator-' + instanceId);
@@ -68,11 +69,8 @@ export async function initializeViewer(config, instanceId) {
68
  canvas.id = canvasId;
69
  canvas.className = 'ply-canvas';
70
  canvas.style.zIndex = "1";
71
- canvas.style.display = "block";
72
- canvas.style.background = "#222"; // Ensure not transparent
73
  viewerContainer.insertBefore(canvas, progressDialog);
74
 
75
- // Scroll/wheel zoom (all platforms)
76
  canvas.addEventListener('wheel', e => {
77
  if (cameraEntity && cameraEntity.script && cameraEntity.script.orbitCamera) {
78
  const orbitCam = cameraEntity.script.orbitCamera;
@@ -89,9 +87,9 @@ export async function initializeViewer(config, instanceId) {
89
 
90
  progressDialog.style.display = 'block';
91
 
92
- // PlayCanvas import
93
  if (!pc) {
94
- pc = await import("https://esm.run/playcanvas@1.68.4"); // Stick to a known-good, stable version!
95
  window.pc = pc;
96
  logAndAlert('[viewer.js] PlayCanvas module loaded.');
97
  }
@@ -99,11 +97,14 @@ export async function initializeViewer(config, instanceId) {
99
  try {
100
  logAndAlert('[viewer.js] Creating graphics device...');
101
  const device = await pc.createGraphicsDevice(canvas, {
102
- deviceTypes: ["webgl2", "webgl1"],
103
- antialias: true,
104
- preserveDrawingBuffer: isIOS(), // True on iOS, false elsewhere
 
 
105
  alpha: false
106
  });
 
107
  device.maxPixelRatio = Math.min(window.devicePixelRatio, 2);
108
 
109
  const opts = new pc.AppOptions();
@@ -126,23 +127,18 @@ export async function initializeViewer(config, instanceId) {
126
  pc.GSplatHandler
127
  ];
128
 
129
- // Use new Application constructor only!
130
  app = new pc.Application(canvas, opts);
131
  app.setCanvasFillMode(pc.FILLMODE_NONE);
132
  app.setCanvasResolution(pc.RESOLUTION_AUTO);
133
 
134
- // iOS: ensure clearColor is NOT transparent, and toneMapping is standard
135
- app.scene.exposure = 1.1;
136
- app.scene.toneMapping = pc.TONEMAP_ACES;
137
- app.scene.gammaCorrection = pc.GAMMA_SRGB;
138
-
139
  logAndAlert('[viewer.js] PlayCanvas application created!');
140
 
141
- // Resize support
142
  resizeObserver = new ResizeObserver(entries => {
143
  for (const entry of entries) {
144
  const { width, height } = entry.contentRect;
145
- if (app) app.resizeCanvas(width, height);
 
 
146
  }
147
  });
148
  resizeObserver.observe(viewerContainer);
@@ -154,27 +150,26 @@ export async function initializeViewer(config, instanceId) {
154
  window.removeEventListener('resize', resizeCanvas);
155
  });
156
 
157
- // ASSETS
158
- // Use a neutral skybox/texture; iOS has issues with "RGBP" PNG as HDR
159
  const assets = {
160
  model: new pc.Asset('gsplat', 'gsplat', { url: plyUrl }),
161
  orbit: new pc.Asset('script', 'script', { url: "https://mikafil-viewer-gs.static.hf.space/orbit-camera.js" }),
162
  galerie: new pc.Asset('galerie', 'container', { url: glbUrl }),
163
  hdr: new pc.Asset('hdr', 'texture', {
164
- url: `https://cdn.jsdelivr.net/gh/playcanvas/playcanvas.github.io@master/static/assets/textures/skybox/skybox_hdr_03.hdr`
165
- }, { type: pc.TEXTURETYPE_RGBM, mipmaps: false })
166
  };
167
 
168
- // Loader
169
  const loader = new pc.AssetListLoader(Object.values(assets), app.assets);
170
  let lastProg = 0;
171
 
172
- // iOS fallback: PLYs can hang
173
  if (isIOS()) {
 
174
  loadTimeout = setTimeout(() => {
175
  alert("⚠️ 3D model loading is taking unusually long on your device. Large PLY files (>40MB) may exceed memory limits on iOS Safari and cannot be shown. Try a smaller model, or use a desktop browser for best results.");
176
- progressDialog.innerHTML = `<p style="color:red">Loading failed or too slow. Try a smaller model, or use a desktop browser.</p>`;
177
- }, 20000);
178
  }
179
 
180
  assets.model.on('load', () => {
@@ -182,16 +177,16 @@ export async function initializeViewer(config, instanceId) {
182
  progressDialog.style.display = 'none';
183
  if (assets.model.resource && assets.model.resource._buffer) {
184
  let buf = assets.model.resource._buffer;
185
- logAndAlert(`[viewer.js] PLY/GSplat asset loaded! Buffer byteLength: ${buf.byteLength || buf.length || 'n/a'}`);
186
  } else {
187
- logAndAlert(`[viewer.js] PLY/GSplat asset loaded! [NO BUFFER?]`);
188
  }
189
  });
190
 
191
  assets.model.on('error', err => {
192
  if (loadTimeout) { clearTimeout(loadTimeout); loadTimeout = null; }
193
  logAndAlert('[viewer.js] Error loading model: ' + err);
194
- progressDialog.innerHTML = `<p style="color: red">Error loading model: ${err}</p>`;
195
  });
196
 
197
  const progCheck = setInterval(() => {
@@ -209,18 +204,9 @@ export async function initializeViewer(config, instanceId) {
209
  if (loadTimeout) { clearTimeout(loadTimeout); loadTimeout = null; }
210
  logAndAlert('[viewer.js] All assets loaded; starting app.');
211
  app.start();
 
212
 
213
- // If HDR loads, set as skybox/envAtlas; else fallback to default lighting
214
- if (assets.hdr.resource) {
215
- try {
216
- app.scene.envAtlas = assets.hdr.resource;
217
- logAndAlert('[viewer.js] EnvAtlas set.');
218
- } catch (e) {
219
- logAndAlert('[viewer.js] Failed to set envAtlas: ' + e.message);
220
- }
221
- }
222
-
223
- // Model
224
  modelEntity = new pc.Entity('model');
225
  try {
226
  modelEntity.addComponent('gsplat', { asset: assets.model });
@@ -228,23 +214,33 @@ export async function initializeViewer(config, instanceId) {
228
  modelEntity.setLocalEulerAngles(modelRotationX, modelRotationY, modelRotationZ);
229
  modelEntity.setLocalScale(modelScale, modelScale, modelScale);
230
  app.root.addChild(modelEntity);
231
- logAndAlert(`[viewer.js] Model entity added at [${modelX}, ${modelY}, ${modelZ}] scale ${modelScale}`);
232
  } catch (e) {
233
  logAndAlert('[viewer.js] Error adding model entity: ' + e.message);
234
  }
235
 
236
- // Add strong ambient light (fixes "black" on iOS if env map fails)
237
- const ambient = new pc.Entity("ambient-light");
238
- ambient.addComponent("light", {
239
- type: "directional",
240
- intensity: 1.1,
241
- color: new pc.Color(1,1,1),
242
- castShadows: false
 
 
 
 
 
 
 
 
 
 
243
  });
244
- ambient.setLocalEulerAngles(45, 30, 0);
245
- app.root.addChild(ambient);
246
 
247
- // Scene GLB container (optional)
248
  if (assets.galerie && assets.galerie.resource) {
249
  try {
250
  const galleryEntity = assets.galerie.resource.instantiateRenderEntity();
@@ -255,13 +251,12 @@ export async function initializeViewer(config, instanceId) {
255
  }
256
  }
257
 
258
- // Camera
259
  cameraEntity = new pc.Entity('camera');
260
  cameraEntity.addComponent('camera', {
261
- clearColor: [0.133,0.133,0.133,1.0], // Never transparent
262
- fov: 60,
263
- nearClip: 0.1,
264
- farClip: 1000
265
  });
266
  cameraEntity.setPosition(chosenCameraX, chosenCameraY, chosenCameraZ);
267
  cameraEntity.lookAt(modelEntity.getPosition());
@@ -299,6 +294,24 @@ export async function initializeViewer(config, instanceId) {
299
  app.root.addChild(cameraEntity);
300
  logAndAlert('[viewer.js] Camera entity added to scene!');
301
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
302
  // Final resize
303
  app.resizeCanvas(viewerContainer.clientWidth, viewerContainer.clientHeight);
304
 
@@ -325,7 +338,7 @@ export async function initializeViewer(config, instanceId) {
325
  } catch (error) {
326
  if (loadTimeout) { clearTimeout(loadTimeout); loadTimeout = null; }
327
  logAndAlert("[viewer.js] Error initializing PlayCanvas viewer: " + error.message);
328
- progressDialog.innerHTML = `<p style="color: red">Error loading viewer: ${error.message}</p>`;
329
  }
330
  }
331
 
@@ -390,4 +403,4 @@ export function cleanupViewer() {
390
  resizeObserver.disconnect();
391
  resizeObserver = null;
392
  }
393
- }
 
1
+ // viewer.js
2
 
3
+ let pc; // PlayCanvas
4
  export let app = null;
5
  let cameraEntity = null;
6
  let modelEntity = null;
7
  let viewerInitialized = false;
8
  let resizeObserver = null;
9
+ let loadTimeout = null; // For iOS load fallback
10
 
 
11
  let chosenCameraX, chosenCameraY, chosenCameraZ;
12
  let minZoom, maxZoom, minAngle, maxAngle, minAzimuth, maxAzimuth, minPivotY, minY;
13
  let modelX, modelY, modelZ, modelScale, modelRotationX, modelRotationY, modelRotationZ;
14
  let plyUrl, glbUrl;
15
 
16
+ // iOS detection
17
  function isIOS() {
18
  return /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream;
19
  }
 
25
  export async function initializeViewer(config, instanceId) {
26
  if (viewerInitialized) return;
27
 
28
+ // Config
29
  plyUrl = config.ply_url;
30
  glbUrl = config.glb_url;
31
  minZoom = parseFloat(config.minZoom || "1");
 
57
  chosenCameraY = mobile ? cameraYPhone : cameraY;
58
  chosenCameraZ = mobile ? cameraZPhone : cameraZ;
59
 
60
+ // DOM
61
  const canvasId = 'canvas-' + instanceId;
62
  const progressDialog = document.getElementById('progress-dialog-' + instanceId);
63
  const progressIndicator = document.getElementById('progress-indicator-' + instanceId);
 
69
  canvas.id = canvasId;
70
  canvas.className = 'ply-canvas';
71
  canvas.style.zIndex = "1";
 
 
72
  viewerContainer.insertBefore(canvas, progressDialog);
73
 
 
74
  canvas.addEventListener('wheel', e => {
75
  if (cameraEntity && cameraEntity.script && cameraEntity.script.orbitCamera) {
76
  const orbitCam = cameraEntity.script.orbitCamera;
 
87
 
88
  progressDialog.style.display = 'block';
89
 
90
+ // PlayCanvas
91
  if (!pc) {
92
+ pc = await import("https://esm.run/playcanvas");
93
  window.pc = pc;
94
  logAndAlert('[viewer.js] PlayCanvas module loaded.');
95
  }
 
97
  try {
98
  logAndAlert('[viewer.js] Creating graphics device...');
99
  const device = await pc.createGraphicsDevice(canvas, {
100
+ deviceTypes: ["webgl2"],
101
+ glslangUrl: "https://playcanvas.vercel.app/static/lib/glslang/glslang.js",
102
+ twgslUrl: "https://playcanvas.vercel.app/static/lib/twgsl/twgsl.js",
103
+ antialias: false,
104
+ preserveDrawingBuffer: isIOS() ? true : false,
105
  alpha: false
106
  });
107
+ logAndAlert('[viewer.js] Graphics device created!');
108
  device.maxPixelRatio = Math.min(window.devicePixelRatio, 2);
109
 
110
  const opts = new pc.AppOptions();
 
127
  pc.GSplatHandler
128
  ];
129
 
 
130
  app = new pc.Application(canvas, opts);
131
  app.setCanvasFillMode(pc.FILLMODE_NONE);
132
  app.setCanvasResolution(pc.RESOLUTION_AUTO);
133
 
 
 
 
 
 
134
  logAndAlert('[viewer.js] PlayCanvas application created!');
135
 
 
136
  resizeObserver = new ResizeObserver(entries => {
137
  for (const entry of entries) {
138
  const { width, height } = entry.contentRect;
139
+ if (app) {
140
+ app.resizeCanvas(width, height);
141
+ }
142
  }
143
  });
144
  resizeObserver.observe(viewerContainer);
 
150
  window.removeEventListener('resize', resizeCanvas);
151
  });
152
 
153
+ // Assets
 
154
  const assets = {
155
  model: new pc.Asset('gsplat', 'gsplat', { url: plyUrl }),
156
  orbit: new pc.Asset('script', 'script', { url: "https://mikafil-viewer-gs.static.hf.space/orbit-camera.js" }),
157
  galerie: new pc.Asset('galerie', 'container', { url: glbUrl }),
158
  hdr: new pc.Asset('hdr', 'texture', {
159
+ url: https://huggingface.co/datasets/bilca/ply_files/resolve/main/galeries/blanc.png
160
+ }, { type: pc.TEXTURETYPE_RGBP, mipmaps: false })
161
  };
162
 
 
163
  const loader = new pc.AssetListLoader(Object.values(assets), app.assets);
164
  let lastProg = 0;
165
 
166
+ // iOS PLY load timeout/fallback
167
  if (isIOS()) {
168
+ // 90MB is a big PLY!
169
  loadTimeout = setTimeout(() => {
170
  alert("⚠️ 3D model loading is taking unusually long on your device. Large PLY files (>40MB) may exceed memory limits on iOS Safari and cannot be shown. Try a smaller model, or use a desktop browser for best results.");
171
+ progressDialog.innerHTML = <p style="color:red">Loading failed or too slow. Try a smaller model, or use a desktop browser.</p>;
172
+ }, 20000); // 20 seconds
173
  }
174
 
175
  assets.model.on('load', () => {
 
177
  progressDialog.style.display = 'none';
178
  if (assets.model.resource && assets.model.resource._buffer) {
179
  let buf = assets.model.resource._buffer;
180
+ logAndAlert([viewer.js] PLY/GSplat asset loaded! Buffer byteLength: ${buf.byteLength || buf.length || 'n/a'});
181
  } else {
182
+ logAndAlert([viewer.js] PLY/GSplat asset loaded! [NO BUFFER?]);
183
  }
184
  });
185
 
186
  assets.model.on('error', err => {
187
  if (loadTimeout) { clearTimeout(loadTimeout); loadTimeout = null; }
188
  logAndAlert('[viewer.js] Error loading model: ' + err);
189
+ progressDialog.innerHTML = <p style="color: red">Error loading model: ${err}</p>;
190
  });
191
 
192
  const progCheck = setInterval(() => {
 
204
  if (loadTimeout) { clearTimeout(loadTimeout); loadTimeout = null; }
205
  logAndAlert('[viewer.js] All assets loaded; starting app.');
206
  app.start();
207
+ app.scene.envAtlas = assets.hdr.resource;
208
 
209
+ // Model entity
 
 
 
 
 
 
 
 
 
 
210
  modelEntity = new pc.Entity('model');
211
  try {
212
  modelEntity.addComponent('gsplat', { asset: assets.model });
 
214
  modelEntity.setLocalEulerAngles(modelRotationX, modelRotationY, modelRotationZ);
215
  modelEntity.setLocalScale(modelScale, modelScale, modelScale);
216
  app.root.addChild(modelEntity);
217
+ logAndAlert('[viewer.js] Model entity added to scene!');
218
  } catch (e) {
219
  logAndAlert('[viewer.js] Error adding model entity: ' + e.message);
220
  }
221
 
222
+ // Light
223
+ const dirLight = new pc.Entity('Cascaded Light');
224
+ dirLight.addComponent('light', {
225
+ type: 'directional',
226
+ color: pc.Color.WHITE,
227
+ shadowBias: 0.3,
228
+ normalOffsetBias: 0.2,
229
+ intensity: 1.0,
230
+ soft: true,
231
+ shadowResolution: 4096,
232
+ penumbraSize: 7,
233
+ penumbraFalloff: 1.5,
234
+ shadowSamples: 128,
235
+ shadowBlockerSamples: 16,
236
+ castShadows: true,
237
+ shadowType: pc.SHADOW_PCSS_32F,
238
+ shadowDistance: 1000
239
  });
240
+ dirLight.setLocalEulerAngles(0, 0, 0);
241
+ app.root.addChild(dirLight);
242
 
243
+ // Gallery GLB
244
  if (assets.galerie && assets.galerie.resource) {
245
  try {
246
  const galleryEntity = assets.galerie.resource.instantiateRenderEntity();
 
251
  }
252
  }
253
 
254
+ // Camera setup
255
  cameraEntity = new pc.Entity('camera');
256
  cameraEntity.addComponent('camera', {
257
+ clearColor: config.canvas_background
258
+ ? parseInt(config.canvas_background.substr(1, 2), 16) / 255
259
+ : 0,
 
260
  });
261
  cameraEntity.setPosition(chosenCameraX, chosenCameraY, chosenCameraZ);
262
  cameraEntity.lookAt(modelEntity.getPosition());
 
294
  app.root.addChild(cameraEntity);
295
  logAndAlert('[viewer.js] Camera entity added to scene!');
296
 
297
+ // Reset & constrain updates
298
+ app.once('update', () => {
299
+ logAndAlert('[viewer.js] First app update! Resetting camera.');
300
+ resetViewerCamera();
301
+ });
302
+ app.on('update', dt => {
303
+ if (cameraEntity) {
304
+ const pos = cameraEntity.getPosition();
305
+ if (pos.y < minY) {
306
+ cameraEntity.setPosition(pos.x, minY, pos.z);
307
+ }
308
+ }
309
+ if (!window._pcFirstUpdate) {
310
+ window._pcFirstUpdate = true;
311
+ logAndAlert('[viewer.js] Entered update/render loop!');
312
+ }
313
+ });
314
+
315
  // Final resize
316
  app.resizeCanvas(viewerContainer.clientWidth, viewerContainer.clientHeight);
317
 
 
338
  } catch (error) {
339
  if (loadTimeout) { clearTimeout(loadTimeout); loadTimeout = null; }
340
  logAndAlert("[viewer.js] Error initializing PlayCanvas viewer: " + error.message);
341
+ progressDialog.innerHTML = <p style="color: red">Error loading viewer: ${error.message}</p>;
342
  }
343
  }
344
 
 
403
  resizeObserver.disconnect();
404
  resizeObserver = null;
405
  }
406
+ }