Spaces:
Running
Running
Update viewer.js
Browse files
viewer.js
CHANGED
|
@@ -10,17 +10,19 @@ let bouchonEntity = null;
|
|
| 10 |
let viewerInitialized = false;
|
| 11 |
let resizeObserver = null;
|
| 12 |
|
|
|
|
| 13 |
let matTransparent = null;
|
| 14 |
let matOpaque = null;
|
| 15 |
let tubeTransparent = null;
|
| 16 |
let tubeOpaque = null;
|
| 17 |
|
|
|
|
| 18 |
let chosenCameraX, chosenCameraY, chosenCameraZ;
|
| 19 |
let minZoom, maxZoom, minAngle, maxAngle, minAzimuth, maxAzimuth, minPivotY, minY;
|
| 20 |
let modelX, modelY, modelZ, modelScale, modelRotationX, modelRotationY, modelRotationZ;
|
| 21 |
let glbUrl, glbUrl2, glbUrl3, aoUrl, opacityUrl, thicknessUrl;
|
| 22 |
|
| 23 |
-
//
|
| 24 |
function traverse(entity, callback) {
|
| 25 |
callback(entity);
|
| 26 |
if (entity.children) {
|
|
@@ -34,9 +36,6 @@ export async function initializeViewer(config, instanceId) {
|
|
| 34 |
const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent);
|
| 35 |
const isMobile = isIOS || /Android/i.test(navigator.userAgent);
|
| 36 |
|
| 37 |
-
alert("Step 1: isIOS = " + isIOS + ", isMobile = " + isMobile);
|
| 38 |
-
|
| 39 |
-
// Parse config (hardcode for demo if needed)
|
| 40 |
glbUrl = config.glb_url;
|
| 41 |
glbUrl2 = config.glb_url_2;
|
| 42 |
glbUrl3 = config.glb_url_3;
|
|
@@ -62,20 +61,20 @@ export async function initializeViewer(config, instanceId) {
|
|
| 62 |
|
| 63 |
const cameraX = 0;
|
| 64 |
const cameraY = 0;
|
| 65 |
-
const cameraZ =
|
| 66 |
const cameraXPhone = 0;
|
| 67 |
const cameraYPhone = 0;
|
| 68 |
-
const cameraZPhone =
|
| 69 |
|
| 70 |
chosenCameraX = isMobile ? cameraXPhone : cameraX;
|
| 71 |
chosenCameraY = isMobile ? cameraYPhone : cameraY;
|
| 72 |
chosenCameraZ = isMobile ? cameraZPhone : cameraZ;
|
| 73 |
|
| 74 |
-
|
| 75 |
|
| 76 |
-
// Canvas creation
|
| 77 |
const canvasId = 'canvas-' + instanceId;
|
| 78 |
const progressDialog = document.getElementById('progress-dialog-' + instanceId);
|
|
|
|
| 79 |
const viewerContainer = document.getElementById('viewer-container-' + instanceId);
|
| 80 |
|
| 81 |
let oldCanvas = document.getElementById(canvasId);
|
|
@@ -87,14 +86,13 @@ export async function initializeViewer(config, instanceId) {
|
|
| 87 |
canvas.style.width = "100%";
|
| 88 |
canvas.style.height = "100%";
|
| 89 |
canvas.setAttribute('tabindex', '0');
|
|
|
|
| 90 |
if (progressDialog) {
|
| 91 |
viewerContainer.insertBefore(canvas, progressDialog);
|
| 92 |
} else {
|
| 93 |
viewerContainer.appendChild(canvas);
|
| 94 |
}
|
| 95 |
-
alert("Step 3: Canvas created and inserted.");
|
| 96 |
|
| 97 |
-
// Listeners & touch-action
|
| 98 |
canvas.style.touchAction = "none";
|
| 99 |
canvas.style.webkitTouchCallout = "none";
|
| 100 |
canvas.addEventListener('gesturestart', e => e.preventDefault());
|
|
@@ -102,8 +100,10 @@ export async function initializeViewer(config, instanceId) {
|
|
| 102 |
canvas.addEventListener('gestureend', e => e.preventDefault());
|
| 103 |
canvas.addEventListener('dblclick', e => e.preventDefault());
|
| 104 |
canvas.addEventListener('touchstart', e => { if (e.touches.length > 1) e.preventDefault(); }, { passive: false });
|
| 105 |
-
|
| 106 |
-
|
|
|
|
|
|
|
| 107 |
|
| 108 |
if (progressDialog) progressDialog.style.display = 'block';
|
| 109 |
|
|
@@ -111,67 +111,60 @@ export async function initializeViewer(config, instanceId) {
|
|
| 111 |
pc = await import("https://esm.run/playcanvas");
|
| 112 |
window.pc = pc;
|
| 113 |
}
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
if (isIOS) deviceTypes = ["webgl1", "webgl2"]; // Prefer WebGL1 first
|
| 119 |
-
// Use AppBase + .init() for compatibility
|
| 120 |
-
const gfxOptions = {
|
| 121 |
-
deviceTypes: deviceTypes,
|
| 122 |
glslangUrl: "https://playcanvas.vercel.app/static/lib/glslang/glslang.js",
|
| 123 |
-
twgslUrl: "https://playcanvas.vercel.app/static/lib/twgsl/twgsl.js"
|
| 124 |
-
|
| 125 |
-
|
| 126 |
device.maxPixelRatio = Math.min(window.devicePixelRatio, 2);
|
| 127 |
-
alert("Step 6: Graphics device created.");
|
| 128 |
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
appOptions.keyboard = new pc.Keyboard(document.body);
|
| 135 |
-
|
| 136 |
-
appOptions.componentSystems = [
|
| 137 |
pc.RenderComponentSystem,
|
| 138 |
pc.CameraComponentSystem,
|
| 139 |
pc.LightComponentSystem,
|
| 140 |
-
pc.ScriptComponentSystem
|
| 141 |
-
|
|
|
|
|
|
|
| 142 |
];
|
| 143 |
-
|
| 144 |
pc.TextureHandler,
|
| 145 |
pc.ContainerHandler,
|
| 146 |
-
pc.ScriptHandler
|
| 147 |
-
|
| 148 |
];
|
| 149 |
|
| 150 |
-
app = new pc.
|
| 151 |
-
app.
|
| 152 |
-
|
| 153 |
-
// Fill window, auto-res
|
| 154 |
-
app.setCanvasFillMode(pc.FILLMODE_FILL_WINDOW);
|
| 155 |
app.setCanvasResolution(pc.RESOLUTION_AUTO);
|
| 156 |
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
window.removeEventListener('resize', resize);
|
| 162 |
});
|
| 163 |
-
|
|
|
|
|
|
|
|
|
|
| 164 |
|
| 165 |
-
// Assets
|
| 166 |
const assets = {
|
| 167 |
orbit: new pc.Asset('script', 'script', { url: "https://mikafil-viewer-gs.static.hf.space/orbit-camera.js" }),
|
| 168 |
-
model: new pc.Asset('
|
| 169 |
-
tube: new pc.Asset('
|
| 170 |
-
bouchon: new pc.Asset('
|
| 171 |
-
ao_map: new pc.Asset('
|
| 172 |
-
op_map: new pc.Asset('
|
| 173 |
-
bg_tex: new pc.Asset('
|
| 174 |
-
thickness_map: new pc.Asset('
|
| 175 |
helipad: new pc.Asset(
|
| 176 |
'helipad-env-atlas',
|
| 177 |
'texture',
|
|
@@ -181,18 +174,17 @@ export async function initializeViewer(config, instanceId) {
|
|
| 181 |
};
|
| 182 |
|
| 183 |
for (const key in assets) app.assets.add(assets[key]);
|
| 184 |
-
alert("Step 9: Assets registered.");
|
| 185 |
|
| 186 |
-
// Load
|
| 187 |
assets.helipad.ready(() => {
|
| 188 |
app.scene.envAtlas = assets.helipad.resource;
|
| 189 |
app.scene.skyboxRotation = new pc.Quat().setFromEulerAngles(30, 20, 0);
|
| 190 |
app.scene.skyboxIntensity = 4;
|
| 191 |
app.scene.skyboxMip = 0;
|
| 192 |
-
alert("Step 10: Environment HDR loaded.");
|
| 193 |
|
| 194 |
-
|
| 195 |
-
|
|
|
|
| 196 |
assets.orbit,
|
| 197 |
assets.model,
|
| 198 |
assets.tube,
|
|
@@ -201,112 +193,119 @@ export async function initializeViewer(config, instanceId) {
|
|
| 201 |
assets.op_map,
|
| 202 |
assets.bg_tex,
|
| 203 |
assets.thickness_map
|
| 204 |
-
];
|
| 205 |
-
|
| 206 |
loader.load(() => {
|
| 207 |
-
alert("Step 11: All assets loaded, app started.");
|
| 208 |
app.start();
|
|
|
|
| 209 |
|
| 210 |
-
//
|
| 211 |
const depthLayer = app.scene.layers.getLayerById(pc.LAYERID_DEPTH);
|
| 212 |
app.scene.layers.remove(depthLayer);
|
| 213 |
app.scene.layers.insertOpaque(depthLayer, 2);
|
| 214 |
-
alert("Step 12: Depth layer reordered.");
|
| 215 |
|
| 216 |
-
//
|
| 217 |
modelEntity = assets.model.resource.instantiateRenderEntity();
|
| 218 |
tubeEntity = assets.tube.resource.instantiateRenderEntity();
|
| 219 |
bouchonEntity = assets.bouchon.resource.instantiateRenderEntity();
|
| 220 |
|
| 221 |
app.root.addChild(modelEntity);
|
| 222 |
app.root.addChild(tubeEntity);
|
| 223 |
-
//
|
| 224 |
-
|
| 225 |
-
|
| 226 |
-
|
| 227 |
-
|
| 228 |
-
|
| 229 |
-
|
| 230 |
-
|
| 231 |
-
|
| 232 |
-
|
| 233 |
-
|
| 234 |
-
|
| 235 |
-
|
| 236 |
-
|
| 237 |
-
|
| 238 |
-
|
| 239 |
-
|
| 240 |
-
|
| 241 |
-
|
| 242 |
-
|
| 243 |
-
|
| 244 |
-
|
| 245 |
-
|
| 246 |
-
|
| 247 |
-
|
| 248 |
-
|
| 249 |
-
|
| 250 |
-
|
| 251 |
-
|
| 252 |
-
|
| 253 |
-
|
| 254 |
-
|
| 255 |
-
|
| 256 |
-
|
| 257 |
-
|
| 258 |
-
|
| 259 |
-
|
| 260 |
-
|
| 261 |
-
|
| 262 |
-
|
| 263 |
-
|
| 264 |
-
|
| 265 |
-
|
| 266 |
-
|
| 267 |
-
|
| 268 |
-
|
| 269 |
-
|
| 270 |
-
|
| 271 |
-
|
| 272 |
-
|
| 273 |
-
|
| 274 |
-
|
| 275 |
-
|
| 276 |
-
|
| 277 |
-
|
| 278 |
-
|
| 279 |
-
|
| 280 |
-
|
| 281 |
-
|
| 282 |
-
|
| 283 |
-
|
| 284 |
-
|
| 285 |
-
|
| 286 |
-
|
| 287 |
-
|
| 288 |
-
|
| 289 |
-
|
| 290 |
-
|
| 291 |
-
|
| 292 |
-
|
| 293 |
-
|
| 294 |
-
|
| 295 |
-
|
| 296 |
-
|
| 297 |
-
|
| 298 |
-
|
| 299 |
-
|
| 300 |
-
}
|
| 301 |
-
alert("Step 14: Materials created and assigned.");
|
| 302 |
-
} catch (err) {
|
| 303 |
-
alert("Material assignment failed: " + err);
|
| 304 |
}
|
| 305 |
-
}
|
| 306 |
-
|
| 307 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 308 |
|
| 309 |
-
//
|
| 310 |
modelEntity.setPosition(modelX, modelY, modelZ);
|
| 311 |
modelEntity.setLocalScale(modelScale, modelScale, modelScale);
|
| 312 |
modelEntity.setLocalEulerAngles(modelRotationX, modelRotationY, modelRotationZ);
|
|
@@ -318,25 +317,20 @@ export async function initializeViewer(config, instanceId) {
|
|
| 318 |
bouchonEntity.setPosition(modelX, modelY, modelZ);
|
| 319 |
bouchonEntity.setLocalScale(modelScale, modelScale, modelScale);
|
| 320 |
bouchonEntity.setLocalEulerAngles(modelRotationX, modelRotationY, modelRotationZ);
|
| 321 |
-
alert("Step 15: Transforms set.");
|
| 322 |
|
| 323 |
// 4. Camera & orbit
|
|
|
|
| 324 |
cameraEntity = new pc.Entity('camera');
|
| 325 |
cameraEntity.addComponent('camera', {
|
| 326 |
-
clearColor: new pc.Color(
|
| 327 |
toneMapping: pc.TONEMAP_NEUTRAL
|
| 328 |
});
|
| 329 |
cameraEntity.setPosition(chosenCameraX, chosenCameraY, chosenCameraZ);
|
| 330 |
cameraEntity.lookAt(modelEntity.getPosition());
|
| 331 |
cameraEntity.addComponent('script');
|
| 332 |
-
|
| 333 |
-
|
| 334 |
-
|
| 335 |
-
cameraEntity.camera.requestSceneColorMap(true);
|
| 336 |
-
} catch (err) {
|
| 337 |
-
alert("requestSceneColorMap failed: " + err);
|
| 338 |
-
}
|
| 339 |
-
}
|
| 340 |
|
| 341 |
cameraEntity.script.create('orbitCamera', {
|
| 342 |
attributes: {
|
|
@@ -355,27 +349,29 @@ export async function initializeViewer(config, instanceId) {
|
|
| 355 |
cameraEntity.script.create('orbitCameraInputMouse');
|
| 356 |
cameraEntity.script.create('orbitCameraInputTouch');
|
| 357 |
app.root.addChild(cameraEntity);
|
| 358 |
-
alert("Step 16: Camera & orbit script added.");
|
| 359 |
|
| 360 |
-
//
|
| 361 |
-
const
|
| 362 |
-
|
| 363 |
-
|
| 364 |
-
|
| 365 |
-
|
| 366 |
-
|
| 367 |
-
|
| 368 |
-
|
| 369 |
-
|
| 370 |
-
|
| 371 |
|
| 372 |
-
// Optional: Add debug/test cube (parent to camera for visibility)
|
| 373 |
const bgTex = assets.bg_tex.resource;
|
| 374 |
const bgPlane = new pc.Entity("Plane");
|
| 375 |
bgPlane.addComponent("model", { type: "plane" });
|
| 376 |
-
|
|
|
|
|
|
|
| 377 |
bgPlane.setLocalScale(11, 1, 5.5);
|
| 378 |
bgPlane.setLocalEulerAngles(90, 0, 0);
|
|
|
|
|
|
|
| 379 |
const mat = new pc.StandardMaterial();
|
| 380 |
mat.diffuse = new pc.Color(1, 1, 1);
|
| 381 |
mat.diffuseMap = bgTex;
|
|
@@ -385,13 +381,24 @@ export async function initializeViewer(config, instanceId) {
|
|
| 385 |
mat.useLighting = false;
|
| 386 |
mat.update();
|
| 387 |
bgPlane.model.material = mat;
|
|
|
|
|
|
|
| 388 |
cameraEntity.addChild(bgPlane);
|
| 389 |
-
|
| 390 |
-
|
| 391 |
-
|
| 392 |
-
|
| 393 |
-
|
|
|
|
|
|
|
| 394 |
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 395 |
|
| 396 |
// Tooltips supported if tooltips_url set
|
| 397 |
try {
|
|
@@ -412,15 +419,14 @@ export async function initializeViewer(config, instanceId) {
|
|
| 412 |
} catch (e) {
|
| 413 |
// Tooltips optional, fail silently
|
| 414 |
}
|
|
|
|
| 415 |
viewerInitialized = true;
|
| 416 |
-
alert("Step 20: Viewer successfully initialized!");
|
| 417 |
});
|
| 418 |
});
|
| 419 |
|
| 420 |
app.assets.load(assets.helipad); // Start by loading the HDR
|
| 421 |
-
}
|
| 422 |
|
| 423 |
-
|
| 424 |
|
| 425 |
export function resetViewerCamera() {
|
| 426 |
try {
|
|
@@ -443,7 +449,7 @@ export function resetViewerCamera() {
|
|
| 443 |
|
| 444 |
orbitCam.pivotPoint = modelPos.clone();
|
| 445 |
orbitCam._targetDistance = dist;
|
| 446 |
-
orbitCam._distance
|
| 447 |
|
| 448 |
const rot = tempEnt.getRotation();
|
| 449 |
const fwd = new pc.Vec3();
|
|
@@ -456,10 +462,10 @@ export function resetViewerCamera() {
|
|
| 456 |
rotNoYaw.transformVector(pc.Vec3.FORWARD, fNoYaw);
|
| 457 |
const pitch = Math.atan2(fNoYaw.y, -fNoYaw.z) * pc.math.RAD_TO_DEG;
|
| 458 |
|
| 459 |
-
orbitCam._targetYaw
|
| 460 |
-
orbitCam._yaw
|
| 461 |
orbitCam._targetPitch = pitch;
|
| 462 |
-
orbitCam._pitch
|
| 463 |
if (orbitCam._updatePosition) orbitCam._updatePosition();
|
| 464 |
|
| 465 |
tempEnt.destroy();
|
|
@@ -468,9 +474,14 @@ export function resetViewerCamera() {
|
|
| 468 |
}
|
| 469 |
}
|
| 470 |
|
| 471 |
-
|
|
|
|
|
|
|
|
|
|
| 472 |
export function changeColor(dr, dg, db, er, eg, eb, ei, op, boolTrans) {
|
| 473 |
-
|
|
|
|
|
|
|
| 474 |
if (!matTransparent) return;
|
| 475 |
|
| 476 |
matTransparent.diffuse.set(dr, dg, db);
|
|
@@ -483,21 +494,21 @@ export function changeColor(dr, dg, db, er, eg, eb, ei, op, boolTrans) {
|
|
| 483 |
tubeTransparent.update();
|
| 484 |
|
| 485 |
traverse(modelEntity, node => {
|
| 486 |
-
|
| 487 |
-
|
| 488 |
-
|
|
|
|
| 489 |
}
|
| 490 |
-
}
|
| 491 |
});
|
| 492 |
|
| 493 |
traverse(tubeEntity, node => {
|
| 494 |
-
|
| 495 |
-
|
| 496 |
-
|
|
|
|
| 497 |
}
|
| 498 |
-
}
|
| 499 |
});
|
| 500 |
-
}
|
| 501 |
if (!matOpaque) return;
|
| 502 |
|
| 503 |
matOpaque.diffuse.set(dr, dg, db);
|
|
@@ -506,19 +517,19 @@ export function changeColor(dr, dg, db, er, eg, eb, ei, op, boolTrans) {
|
|
| 506 |
matOpaque.update();
|
| 507 |
|
| 508 |
traverse(modelEntity, node => {
|
| 509 |
-
|
| 510 |
-
|
| 511 |
-
|
|
|
|
| 512 |
}
|
| 513 |
-
}
|
| 514 |
});
|
| 515 |
|
| 516 |
traverse(tubeEntity, node => {
|
| 517 |
-
|
| 518 |
-
|
| 519 |
-
|
|
|
|
| 520 |
}
|
| 521 |
-
}
|
| 522 |
});
|
| 523 |
}
|
| 524 |
}
|
|
|
|
| 10 |
let viewerInitialized = false;
|
| 11 |
let resizeObserver = null;
|
| 12 |
|
| 13 |
+
// These will hold references to the material(s) you want to update.
|
| 14 |
let matTransparent = null;
|
| 15 |
let matOpaque = null;
|
| 16 |
let tubeTransparent = null;
|
| 17 |
let tubeOpaque = null;
|
| 18 |
|
| 19 |
+
|
| 20 |
let chosenCameraX, chosenCameraY, chosenCameraZ;
|
| 21 |
let minZoom, maxZoom, minAngle, maxAngle, minAzimuth, maxAzimuth, minPivotY, minY;
|
| 22 |
let modelX, modelY, modelZ, modelScale, modelRotationX, modelRotationY, modelRotationZ;
|
| 23 |
let glbUrl, glbUrl2, glbUrl3, aoUrl, opacityUrl, thicknessUrl;
|
| 24 |
|
| 25 |
+
// RECURSIVE ENTITY TRAVERSAL (needed for material assignment)
|
| 26 |
function traverse(entity, callback) {
|
| 27 |
callback(entity);
|
| 28 |
if (entity.children) {
|
|
|
|
| 36 |
const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent);
|
| 37 |
const isMobile = isIOS || /Android/i.test(navigator.userAgent);
|
| 38 |
|
|
|
|
|
|
|
|
|
|
| 39 |
glbUrl = config.glb_url;
|
| 40 |
glbUrl2 = config.glb_url_2;
|
| 41 |
glbUrl3 = config.glb_url_3;
|
|
|
|
| 61 |
|
| 62 |
const cameraX = 0;
|
| 63 |
const cameraY = 0;
|
| 64 |
+
const cameraZ = 1;
|
| 65 |
const cameraXPhone = 0;
|
| 66 |
const cameraYPhone = 0;
|
| 67 |
+
const cameraZPhone = 1;
|
| 68 |
|
| 69 |
chosenCameraX = isMobile ? cameraXPhone : cameraX;
|
| 70 |
chosenCameraY = isMobile ? cameraYPhone : cameraY;
|
| 71 |
chosenCameraZ = isMobile ? cameraZPhone : cameraZ;
|
| 72 |
|
| 73 |
+
|
| 74 |
|
|
|
|
| 75 |
const canvasId = 'canvas-' + instanceId;
|
| 76 |
const progressDialog = document.getElementById('progress-dialog-' + instanceId);
|
| 77 |
+
const progressIndicator = document.getElementById('progress-indicator-' + instanceId);
|
| 78 |
const viewerContainer = document.getElementById('viewer-container-' + instanceId);
|
| 79 |
|
| 80 |
let oldCanvas = document.getElementById(canvasId);
|
|
|
|
| 86 |
canvas.style.width = "100%";
|
| 87 |
canvas.style.height = "100%";
|
| 88 |
canvas.setAttribute('tabindex', '0');
|
| 89 |
+
// Safer insert:
|
| 90 |
if (progressDialog) {
|
| 91 |
viewerContainer.insertBefore(canvas, progressDialog);
|
| 92 |
} else {
|
| 93 |
viewerContainer.appendChild(canvas);
|
| 94 |
}
|
|
|
|
| 95 |
|
|
|
|
| 96 |
canvas.style.touchAction = "none";
|
| 97 |
canvas.style.webkitTouchCallout = "none";
|
| 98 |
canvas.addEventListener('gesturestart', e => e.preventDefault());
|
|
|
|
| 100 |
canvas.addEventListener('gestureend', e => e.preventDefault());
|
| 101 |
canvas.addEventListener('dblclick', e => e.preventDefault());
|
| 102 |
canvas.addEventListener('touchstart', e => { if (e.touches.length > 1) e.preventDefault(); }, { passive: false });
|
| 103 |
+
|
| 104 |
+
canvas.addEventListener('wheel', (e) => {
|
| 105 |
+
e.preventDefault();
|
| 106 |
+
}, { passive: false });
|
| 107 |
|
| 108 |
if (progressDialog) progressDialog.style.display = 'block';
|
| 109 |
|
|
|
|
| 111 |
pc = await import("https://esm.run/playcanvas");
|
| 112 |
window.pc = pc;
|
| 113 |
}
|
| 114 |
+
|
| 115 |
+
// Create app first
|
| 116 |
+
const device = await pc.createGraphicsDevice(canvas, {
|
| 117 |
+
deviceTypes: ["webgl2"],
|
|
|
|
|
|
|
|
|
|
|
|
|
| 118 |
glslangUrl: "https://playcanvas.vercel.app/static/lib/glslang/glslang.js",
|
| 119 |
+
twgslUrl: "https://playcanvas.vercel.app/static/lib/twgsl/twgsl.js",
|
| 120 |
+
antialias: false
|
| 121 |
+
});
|
| 122 |
device.maxPixelRatio = Math.min(window.devicePixelRatio, 2);
|
|
|
|
| 123 |
|
| 124 |
+
const opts = new pc.AppOptions();
|
| 125 |
+
opts.graphicsDevice = device;
|
| 126 |
+
opts.mouse = new pc.Mouse(canvas);
|
| 127 |
+
opts.touch = new pc.TouchDevice(canvas);
|
| 128 |
+
opts.componentSystems = [
|
|
|
|
|
|
|
|
|
|
| 129 |
pc.RenderComponentSystem,
|
| 130 |
pc.CameraComponentSystem,
|
| 131 |
pc.LightComponentSystem,
|
| 132 |
+
pc.ScriptComponentSystem,
|
| 133 |
+
pc.GSplatComponentSystem,
|
| 134 |
+
pc.CollisionComponentSystem,
|
| 135 |
+
pc.RigidbodyComponentSystem
|
| 136 |
];
|
| 137 |
+
opts.resourceHandlers = [
|
| 138 |
pc.TextureHandler,
|
| 139 |
pc.ContainerHandler,
|
| 140 |
+
pc.ScriptHandler,
|
| 141 |
+
pc.GSplatHandler
|
| 142 |
];
|
| 143 |
|
| 144 |
+
app = new pc.Application(canvas, opts);
|
| 145 |
+
app.setCanvasFillMode(pc.FILLMODE_NONE);
|
|
|
|
|
|
|
|
|
|
| 146 |
app.setCanvasResolution(pc.RESOLUTION_AUTO);
|
| 147 |
|
| 148 |
+
resizeObserver = new ResizeObserver(entries => {
|
| 149 |
+
entries.forEach(entry => {
|
| 150 |
+
app.resizeCanvas(entry.contentRect.width, entry.contentRect.height);
|
| 151 |
+
});
|
|
|
|
| 152 |
});
|
| 153 |
+
resizeObserver.observe(viewerContainer);
|
| 154 |
+
|
| 155 |
+
window.addEventListener('resize', () => app.resizeCanvas(viewerContainer.clientWidth, viewerContainer.clientHeight));
|
| 156 |
+
app.on('destroy', () => resizeObserver.disconnect());
|
| 157 |
|
| 158 |
+
// --- Assets ---
|
| 159 |
const assets = {
|
| 160 |
orbit: new pc.Asset('script', 'script', { url: "https://mikafil-viewer-gs.static.hf.space/orbit-camera.js" }),
|
| 161 |
+
model: new pc.Asset('glb', 'container', { url: glbUrl }),
|
| 162 |
+
tube: new pc.Asset('glb', 'container', { url: glbUrl2 }),
|
| 163 |
+
bouchon: new pc.Asset('glb', 'container', { url: glbUrl3 }),
|
| 164 |
+
ao_map: new pc.Asset('color', 'texture', { url: aoUrl }),
|
| 165 |
+
op_map: new pc.Asset('color', 'texture', { url: opacityUrl }),
|
| 166 |
+
bg_tex: new pc.Asset('color', 'texture', {url: "https://huggingface.co/datasets/MikaFil/3D_models/resolve/main/EARCARE/banniere_earcare.png"}),
|
| 167 |
+
thickness_map: new pc.Asset('color', 'texture', { url: thicknessUrl }),
|
| 168 |
helipad: new pc.Asset(
|
| 169 |
'helipad-env-atlas',
|
| 170 |
'texture',
|
|
|
|
| 174 |
};
|
| 175 |
|
| 176 |
for (const key in assets) app.assets.add(assets[key]);
|
|
|
|
| 177 |
|
| 178 |
+
// Load HDR *before* loading any models
|
| 179 |
assets.helipad.ready(() => {
|
| 180 |
app.scene.envAtlas = assets.helipad.resource;
|
| 181 |
app.scene.skyboxRotation = new pc.Quat().setFromEulerAngles(30, 20, 0);
|
| 182 |
app.scene.skyboxIntensity = 4;
|
| 183 |
app.scene.skyboxMip = 0;
|
|
|
|
| 184 |
|
| 185 |
+
|
| 186 |
+
// Now load all GLBs and other assets
|
| 187 |
+
const loader = new pc.AssetListLoader([
|
| 188 |
assets.orbit,
|
| 189 |
assets.model,
|
| 190 |
assets.tube,
|
|
|
|
| 193 |
assets.op_map,
|
| 194 |
assets.bg_tex,
|
| 195 |
assets.thickness_map
|
| 196 |
+
], app.assets);
|
| 197 |
+
|
| 198 |
loader.load(() => {
|
|
|
|
| 199 |
app.start();
|
| 200 |
+
if (progressDialog) progressDialog.style.display = 'none';
|
| 201 |
|
| 202 |
+
// Reorder depth layer for transmission
|
| 203 |
const depthLayer = app.scene.layers.getLayerById(pc.LAYERID_DEPTH);
|
| 204 |
app.scene.layers.remove(depthLayer);
|
| 205 |
app.scene.layers.insertOpaque(depthLayer, 2);
|
|
|
|
| 206 |
|
| 207 |
+
// 1. Models
|
| 208 |
modelEntity = assets.model.resource.instantiateRenderEntity();
|
| 209 |
tubeEntity = assets.tube.resource.instantiateRenderEntity();
|
| 210 |
bouchonEntity = assets.bouchon.resource.instantiateRenderEntity();
|
| 211 |
|
| 212 |
app.root.addChild(modelEntity);
|
| 213 |
app.root.addChild(tubeEntity);
|
| 214 |
+
//app.root.addChild(bouchonEntity);
|
| 215 |
+
|
| 216 |
+
// 2. Materials for main model & tube
|
| 217 |
+
|
| 218 |
+
|
| 219 |
+
//the 2 materials for the main model :
|
| 220 |
+
const ao = assets.ao_map.resource;
|
| 221 |
+
const op = assets.op_map.resource;
|
| 222 |
+
const thickness = assets.thickness_map.resource;
|
| 223 |
+
|
| 224 |
+
//mat transparent
|
| 225 |
+
matTransparent = new pc.StandardMaterial();
|
| 226 |
+
matTransparent.blendType = pc.BLEND_NORMAL;
|
| 227 |
+
matTransparent.diffuse = new pc.Color(0.7, 0.05, 0.05);
|
| 228 |
+
matTransparent.specular = new pc.Color(0.01, 0.01, 0.01);
|
| 229 |
+
matTransparent.shininess = 90;
|
| 230 |
+
matTransparent.gloss = 1;
|
| 231 |
+
matTransparent.metalness = 0.0;
|
| 232 |
+
matTransparent.useMetalness = true;
|
| 233 |
+
matTransparent.useDynamicRefraction = true;
|
| 234 |
+
matTransparent.depthWrite = true;
|
| 235 |
+
matTransparent.refraction= 0.8;
|
| 236 |
+
matTransparent.refractionIndex = 1;
|
| 237 |
+
matTransparent.thickness = 0.02;
|
| 238 |
+
matTransparent.thicknessMap = thickness;
|
| 239 |
+
matTransparent.opacityMap = op;
|
| 240 |
+
matTransparent.opacityMapChannel = "r";
|
| 241 |
+
matTransparent.opacity = 0.97;
|
| 242 |
+
matTransparent.emissive = new pc.Color(0.372, 0.03, 0.003);
|
| 243 |
+
matTransparent.emissiveMap = ao;
|
| 244 |
+
matTransparent.emissiveIntensity = 0.1;
|
| 245 |
+
matTransparent.update();
|
| 246 |
+
|
| 247 |
+
|
| 248 |
+
//mat opaque
|
| 249 |
+
matOpaque = new pc.StandardMaterial();
|
| 250 |
+
matOpaque.blendType = pc.BLEND_NORMAL;
|
| 251 |
+
matOpaque.diffuse = new pc.Color(0.7, 0.05, 0.05);
|
| 252 |
+
matOpaque.specular = new pc.Color(0.01, 0.01, 0.01);
|
| 253 |
+
matOpaque.shininess = 90;
|
| 254 |
+
matOpaque.gloss = 1;
|
| 255 |
+
matOpaque.metalness = 0.0;
|
| 256 |
+
matOpaque.opacityMap = op;
|
| 257 |
+
matOpaque.opacityMapChannel = "r";
|
| 258 |
+
matOpaque.opacity = 1;
|
| 259 |
+
matOpaque.emissive = new pc.Color(0.372, 0.03, 0.003);
|
| 260 |
+
matOpaque.emissiveMap = ao;
|
| 261 |
+
matOpaque.emissiveIntensity = 2;
|
| 262 |
+
matOpaque.update();
|
| 263 |
+
|
| 264 |
+
|
| 265 |
+
|
| 266 |
+
|
| 267 |
+
//material for the tube
|
| 268 |
+
//transparent
|
| 269 |
+
tubeTransparent = new pc.StandardMaterial();
|
| 270 |
+
tubeTransparent.diffuse = new pc.Color(1, 1, 1);
|
| 271 |
+
tubeTransparent.blendType = pc.BLEND_NORMAL;
|
| 272 |
+
tubeTransparent.opacity = 0.1;
|
| 273 |
+
tubeTransparent.depthTest = false;
|
| 274 |
+
tubeTransparent.depthWrite = false;
|
| 275 |
+
tubeTransparent.useMetalness = true;
|
| 276 |
+
tubeTransparent.useDynamicRefraction = true;
|
| 277 |
+
tubeTransparent.thickness = 4;
|
| 278 |
+
tubeTransparent.update();
|
| 279 |
+
|
| 280 |
+
|
| 281 |
+
//opaque
|
| 282 |
+
tubeOpaque = new pc.StandardMaterial();
|
| 283 |
+
tubeOpaque.diffuse = new pc.Color(1, 1, 1);
|
| 284 |
+
tubeOpaque.opacity = 0.9;
|
| 285 |
+
tubeOpaque.update();
|
| 286 |
+
|
| 287 |
+
traverse(modelEntity, node => {
|
| 288 |
+
if (node.render && node.render.meshInstances) {
|
| 289 |
+
for (let mi of node.render.meshInstances) {
|
| 290 |
+
mi.material = matOpaque;
|
| 291 |
+
}
|
|
|
|
|
|
|
|
|
|
| 292 |
}
|
| 293 |
+
});
|
| 294 |
+
|
| 295 |
+
|
| 296 |
+
traverse(tubeEntity, node => {
|
| 297 |
+
if (node.render && node.render.meshInstances) {
|
| 298 |
+
for (let mi of node.render.meshInstances) {
|
| 299 |
+
mi.material = tubeOpaque;
|
| 300 |
+
}
|
| 301 |
+
}
|
| 302 |
+
});
|
| 303 |
+
|
| 304 |
+
|
| 305 |
+
|
| 306 |
+
// Do NOT override materials for bouchonEntity!
|
| 307 |
|
| 308 |
+
// 3. Position/scale/orientation
|
| 309 |
modelEntity.setPosition(modelX, modelY, modelZ);
|
| 310 |
modelEntity.setLocalScale(modelScale, modelScale, modelScale);
|
| 311 |
modelEntity.setLocalEulerAngles(modelRotationX, modelRotationY, modelRotationZ);
|
|
|
|
| 317 |
bouchonEntity.setPosition(modelX, modelY, modelZ);
|
| 318 |
bouchonEntity.setLocalScale(modelScale, modelScale, modelScale);
|
| 319 |
bouchonEntity.setLocalEulerAngles(modelRotationX, modelRotationY, modelRotationZ);
|
|
|
|
| 320 |
|
| 321 |
// 4. Camera & orbit
|
| 322 |
+
|
| 323 |
cameraEntity = new pc.Entity('camera');
|
| 324 |
cameraEntity.addComponent('camera', {
|
| 325 |
+
clearColor: new pc.Color(1, 1, 1, 1),
|
| 326 |
toneMapping: pc.TONEMAP_NEUTRAL
|
| 327 |
});
|
| 328 |
cameraEntity.setPosition(chosenCameraX, chosenCameraY, chosenCameraZ);
|
| 329 |
cameraEntity.lookAt(modelEntity.getPosition());
|
| 330 |
cameraEntity.addComponent('script');
|
| 331 |
+
|
| 332 |
+
// --- **CRITICAL** for KHR_materials_transmission! ---
|
| 333 |
+
cameraEntity.camera.requestSceneColorMap(true);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 334 |
|
| 335 |
cameraEntity.script.create('orbitCamera', {
|
| 336 |
attributes: {
|
|
|
|
| 349 |
cameraEntity.script.create('orbitCameraInputMouse');
|
| 350 |
cameraEntity.script.create('orbitCameraInputTouch');
|
| 351 |
app.root.addChild(cameraEntity);
|
|
|
|
| 352 |
|
| 353 |
+
// Remove the Skybox layer from the camera
|
| 354 |
+
const skyboxLayer = app.scene.layers.getLayerByName("Skybox");
|
| 355 |
+
const camLayers = cameraEntity.camera.layers.slice();
|
| 356 |
+
const idx = camLayers.indexOf(skyboxLayer.id);
|
| 357 |
+
if (idx !== -1) {
|
| 358 |
+
camLayers.splice(idx, 1);
|
| 359 |
+
cameraEntity.camera.layers = camLayers;
|
| 360 |
+
}
|
| 361 |
+
|
| 362 |
+
// --- Add this block for a cube parented to the camera ---
|
| 363 |
+
// Create the cube
|
| 364 |
|
|
|
|
| 365 |
const bgTex = assets.bg_tex.resource;
|
| 366 |
const bgPlane = new pc.Entity("Plane");
|
| 367 |
bgPlane.addComponent("model", { type: "plane" });
|
| 368 |
+
// Set the cube's local position relative to the camera
|
| 369 |
+
bgPlane.setLocalPosition(0, 0, -10); // 2 units in front of camera
|
| 370 |
+
// Optionally, scale or color the cube
|
| 371 |
bgPlane.setLocalScale(11, 1, 5.5);
|
| 372 |
bgPlane.setLocalEulerAngles(90, 0, 0);
|
| 373 |
+
|
| 374 |
+
// Simple material for visibility
|
| 375 |
const mat = new pc.StandardMaterial();
|
| 376 |
mat.diffuse = new pc.Color(1, 1, 1);
|
| 377 |
mat.diffuseMap = bgTex;
|
|
|
|
| 381 |
mat.useLighting = false;
|
| 382 |
mat.update();
|
| 383 |
bgPlane.model.material = mat;
|
| 384 |
+
|
| 385 |
+
// Parent to the camera
|
| 386 |
cameraEntity.addChild(bgPlane);
|
| 387 |
+
|
| 388 |
+
// 5. Light
|
| 389 |
+
const light = new pc.Entity("mainLight");
|
| 390 |
+
light.addComponent('light',{
|
| 391 |
+
type: "directional",
|
| 392 |
+
color: new pc.Color(1, 1, 1),
|
| 393 |
+
intensity: 0.9,
|
| 394 |
});
|
| 395 |
+
light.setPosition(0, 1, -1);
|
| 396 |
+
light.lookAt(0, 0, 0);
|
| 397 |
+
app.root.addChild(light);
|
| 398 |
+
|
| 399 |
+
app.resizeCanvas(viewerContainer.clientWidth, viewerContainer.clientHeight);
|
| 400 |
+
|
| 401 |
+
app.once('update', () => resetViewerCamera());
|
| 402 |
|
| 403 |
// Tooltips supported if tooltips_url set
|
| 404 |
try {
|
|
|
|
| 419 |
} catch (e) {
|
| 420 |
// Tooltips optional, fail silently
|
| 421 |
}
|
| 422 |
+
|
| 423 |
viewerInitialized = true;
|
|
|
|
| 424 |
});
|
| 425 |
});
|
| 426 |
|
| 427 |
app.assets.load(assets.helipad); // Start by loading the HDR
|
|
|
|
| 428 |
|
| 429 |
+
}
|
| 430 |
|
| 431 |
export function resetViewerCamera() {
|
| 432 |
try {
|
|
|
|
| 449 |
|
| 450 |
orbitCam.pivotPoint = modelPos.clone();
|
| 451 |
orbitCam._targetDistance = dist;
|
| 452 |
+
orbitCam._distance = dist;
|
| 453 |
|
| 454 |
const rot = tempEnt.getRotation();
|
| 455 |
const fwd = new pc.Vec3();
|
|
|
|
| 462 |
rotNoYaw.transformVector(pc.Vec3.FORWARD, fNoYaw);
|
| 463 |
const pitch = Math.atan2(fNoYaw.y, -fNoYaw.z) * pc.math.RAD_TO_DEG;
|
| 464 |
|
| 465 |
+
orbitCam._targetYaw = yaw;
|
| 466 |
+
orbitCam._yaw = yaw;
|
| 467 |
orbitCam._targetPitch = pitch;
|
| 468 |
+
orbitCam._pitch = pitch;
|
| 469 |
if (orbitCam._updatePosition) orbitCam._updatePosition();
|
| 470 |
|
| 471 |
tempEnt.destroy();
|
|
|
|
| 474 |
}
|
| 475 |
}
|
| 476 |
|
| 477 |
+
/**
|
| 478 |
+
* Change the main model's diffuse color in real time.
|
| 479 |
+
* r,g,b are floats [0,1]. This is a reusable function for new colors.
|
| 480 |
+
*/
|
| 481 |
export function changeColor(dr, dg, db, er, eg, eb, ei, op, boolTrans) {
|
| 482 |
+
|
| 483 |
+
//cameraEntity.camera.clearColor = new pc.Color(colorBgR, colorBgG, colorBgB);
|
| 484 |
+
if(boolTrans){
|
| 485 |
if (!matTransparent) return;
|
| 486 |
|
| 487 |
matTransparent.diffuse.set(dr, dg, db);
|
|
|
|
| 494 |
tubeTransparent.update();
|
| 495 |
|
| 496 |
traverse(modelEntity, node => {
|
| 497 |
+
if (node.render && node.render.meshInstances) {
|
| 498 |
+
for (let mi of node.render.meshInstances) {
|
| 499 |
+
mi.material = matTransparent;
|
| 500 |
+
}
|
| 501 |
}
|
|
|
|
| 502 |
});
|
| 503 |
|
| 504 |
traverse(tubeEntity, node => {
|
| 505 |
+
if (node.render && node.render.meshInstances) {
|
| 506 |
+
for (let mi of node.render.meshInstances) {
|
| 507 |
+
mi.material = tubeTransparent;
|
| 508 |
+
}
|
| 509 |
}
|
|
|
|
| 510 |
});
|
| 511 |
+
}else{
|
| 512 |
if (!matOpaque) return;
|
| 513 |
|
| 514 |
matOpaque.diffuse.set(dr, dg, db);
|
|
|
|
| 517 |
matOpaque.update();
|
| 518 |
|
| 519 |
traverse(modelEntity, node => {
|
| 520 |
+
if (node.render && node.render.meshInstances) {
|
| 521 |
+
for (let mi of node.render.meshInstances) {
|
| 522 |
+
mi.material = matOpaque;
|
| 523 |
+
}
|
| 524 |
}
|
|
|
|
| 525 |
});
|
| 526 |
|
| 527 |
traverse(tubeEntity, node => {
|
| 528 |
+
if (node.render && node.render.meshInstances) {
|
| 529 |
+
for (let mi of node.render.meshInstances) {
|
| 530 |
+
mi.material = tubeOpaque;
|
| 531 |
+
}
|
| 532 |
}
|
|
|
|
| 533 |
});
|
| 534 |
}
|
| 535 |
}
|