visualiseur_EARCARE / viewer.js
MikaFil's picture
Update viewer.js
84cc625 verified
raw
history blame
17.4 kB
// viewer.js
// ==============================
let pc;
export let app = null;
let cameraEntity = null;
let modelEntity = null;
let tubeEntity = null;
let bouchonEntity = null;
let viewerInitialized = false;
let resizeObserver = null;
// These will hold references to the material(s) you want to update.
let matTransparent = null;
let matOpaque = null;
let tubeTransparent = null;
let tubeOpaque = null;
let chosenCameraX, chosenCameraY, chosenCameraZ;
let minZoom, maxZoom, minAngle, maxAngle, minAzimuth, maxAzimuth, minPivotY, minY;
let modelX, modelY, modelZ, modelScale, modelRotationX, modelRotationY, modelRotationZ;
let glbUrl, glbUrl2, glbUrl3, aoUrl, opacityUrl, thicknessUrl;
// RECURSIVE ENTITY TRAVERSAL (needed for material assignment)
function traverse(entity, callback) {
callback(entity);
if (entity.children) {
entity.children.forEach(child => traverse(child, callback));
}
}
export async function initializeViewer(config, instanceId) {
if (viewerInitialized) return;
const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent);
const isMobile = isIOS || /Android/i.test(navigator.userAgent);
glbUrl = config.glb_url;
glbUrl2 = config.glb_url_2;
glbUrl3 = config.glb_url_3;
aoUrl = config.ao_url;
thicknessUrl = config.thickness_url;
opacityUrl = config.opacity_url;
minZoom = 0.2;
maxZoom = 2;
minAngle = -Infinity;
maxAngle = Infinity;
minAzimuth = -Infinity;
maxAzimuth = Infinity;
minPivotY = 0;
minY = -10;
modelX = 0;
modelY = 0;
modelZ = 0;
modelScale = 1;
modelRotationX = 0;
modelRotationY = 0;
modelRotationZ = 0;
const cameraX = 0;
const cameraY = 0;
const cameraZ = 1;
const cameraXPhone = 0;
const cameraYPhone = 0;
const cameraZPhone = 1;
chosenCameraX = isMobile ? cameraXPhone : cameraX;
chosenCameraY = isMobile ? cameraYPhone : cameraY;
chosenCameraZ = isMobile ? cameraZPhone : cameraZ;
const canvasId = 'canvas-' + instanceId;
const progressDialog = document.getElementById('progress-dialog-' + instanceId);
const progressIndicator = document.getElementById('progress-indicator-' + instanceId);
const viewerContainer = document.getElementById('viewer-container-' + instanceId);
let oldCanvas = document.getElementById(canvasId);
if (oldCanvas) oldCanvas.remove();
const canvas = document.createElement('canvas');
canvas.id = canvasId;
canvas.className = 'ply-canvas';
canvas.style.width = "100%";
canvas.style.height = "100%";
canvas.setAttribute('tabindex', '0');
// Safer insert:
if (progressDialog) {
viewerContainer.insertBefore(canvas, progressDialog);
} else {
viewerContainer.appendChild(canvas);
}
canvas.style.touchAction = "none";
canvas.style.webkitTouchCallout = "none";
canvas.addEventListener('gesturestart', e => e.preventDefault());
canvas.addEventListener('gesturechange', e => e.preventDefault());
canvas.addEventListener('gestureend', e => e.preventDefault());
canvas.addEventListener('dblclick', e => e.preventDefault());
canvas.addEventListener('touchstart', e => { if (e.touches.length > 1) e.preventDefault(); }, { passive: false });
canvas.addEventListener('wheel', (e) => {
e.preventDefault();
}, { passive: false });
if (progressDialog) progressDialog.style.display = 'block';
if (!pc) {
pc = await import("https://esm.run/playcanvas");
window.pc = pc;
}
// Create app first
const device = await pc.createGraphicsDevice(canvas, {
deviceTypes: ["webgl2"],
glslangUrl: "https://playcanvas.vercel.app/static/lib/glslang/glslang.js",
twgslUrl: "https://playcanvas.vercel.app/static/lib/twgsl/twgsl.js",
antialias: false
});
device.maxPixelRatio = Math.min(window.devicePixelRatio, 2);
const opts = new pc.AppOptions();
opts.graphicsDevice = device;
opts.mouse = new pc.Mouse(canvas);
opts.touch = new pc.TouchDevice(canvas);
opts.componentSystems = [
pc.RenderComponentSystem,
pc.CameraComponentSystem,
pc.LightComponentSystem,
pc.ScriptComponentSystem,
pc.GSplatComponentSystem,
pc.CollisionComponentSystem,
pc.RigidbodyComponentSystem
];
opts.resourceHandlers = [
pc.TextureHandler,
pc.ContainerHandler,
pc.ScriptHandler,
pc.GSplatHandler
];
app = new pc.Application(canvas, opts);
app.setCanvasFillMode(pc.FILLMODE_NONE);
app.setCanvasResolution(pc.RESOLUTION_AUTO);
resizeObserver = new ResizeObserver(entries => {
entries.forEach(entry => {
app.resizeCanvas(entry.contentRect.width, entry.contentRect.height);
});
});
resizeObserver.observe(viewerContainer);
window.addEventListener('resize', () => app.resizeCanvas(viewerContainer.clientWidth, viewerContainer.clientHeight));
app.on('destroy', () => resizeObserver.disconnect());
// --- Assets ---
const assets = {
orbit: new pc.Asset('script', 'script', { url: "https://mikafil-viewer-gs.static.hf.space/orbit-camera.js" }),
model: new pc.Asset('glb', 'container', { url: glbUrl }),
tube: new pc.Asset('glb', 'container', { url: glbUrl2 }),
bouchon: new pc.Asset('glb', 'container', { url: glbUrl3 }),
ao_map: new pc.Asset('color', 'texture', { url: aoUrl }),
op_map: new pc.Asset('color', 'texture', { url: opacityUrl }),
bg_tex: new pc.Asset('color', 'texture', {url: "https://huggingface.co/datasets/MikaFil/3D_models/resolve/main/EARCARE/banniere_earcare.png"}),
thickness_map: new pc.Asset('color', 'texture', { url: thicknessUrl }),
helipad: new pc.Asset(
'helipad-env-atlas',
'texture',
{ url: "https://huggingface.co/datasets/MikaFil/3D_models/resolve/main/EARCARE/hdr/ciel_nuageux_1k.png" },
{ type: pc.TEXTURETYPE_RGBP, mipmaps: false }
)
};
for (const key in assets) app.assets.add(assets[key]);
// Load HDR *before* loading any models
assets.helipad.ready(() => {
app.scene.envAtlas = assets.helipad.resource;
app.scene.skyboxRotation = new pc.Quat().setFromEulerAngles(30, 20, 0);
app.scene.skyboxIntensity = 4;
app.scene.skyboxMip = 0;
// Now load all GLBs and other assets
const loader = new pc.AssetListLoader([
assets.orbit,
assets.model,
assets.tube,
assets.bouchon,
assets.ao_map,
assets.op_map,
assets.bg_tex,
assets.thickness_map
], app.assets);
loader.load(() => {
app.start();
if (progressDialog) progressDialog.style.display = 'none';
// Reorder depth layer for transmission
const depthLayer = app.scene.layers.getLayerById(pc.LAYERID_DEPTH);
app.scene.layers.remove(depthLayer);
app.scene.layers.insertOpaque(depthLayer, 2);
// 1. Models
modelEntity = assets.model.resource.instantiateRenderEntity();
tubeEntity = assets.tube.resource.instantiateRenderEntity();
bouchonEntity = assets.bouchon.resource.instantiateRenderEntity();
app.root.addChild(modelEntity);
app.root.addChild(tubeEntity);
//app.root.addChild(bouchonEntity);
// 2. Materials for main model & tube
//the 2 materials for the main model :
const ao = assets.ao_map.resource;
const op = assets.op_map.resource;
const thickness = assets.thickness_map.resource;
//mat transparent
matTransparent = new pc.StandardMaterial();
matTransparent.blendType = pc.BLEND_NORMAL;
matTransparent.diffuse = new pc.Color(0.7, 0.05, 0.05);
matTransparent.specular = new pc.Color(0.01, 0.01, 0.01);
matTransparent.shininess = 90;
matTransparent.gloss = 1;
matTransparent.metalness = 0.0;
matTransparent.useMetalness = true;
matTransparent.useDynamicRefraction = true;
matTransparent.depthWrite = true;
matTransparent.refraction= 0.8;
matTransparent.refractionIndex = 1;
matTransparent.thickness = 0.02;
matTransparent.thicknessMap = thickness;
matTransparent.opacityMap = op;
matTransparent.opacityMapChannel = "r";
matTransparent.opacity = 0.97;
matTransparent.emissive = new pc.Color(0.372, 0.03, 0.003);
matTransparent.emissiveMap = ao;
matTransparent.emissiveIntensity = 1;
matTransparent.update();
//mat opaque
matOpaque = new pc.StandardMaterial();
matOpaque.blendType = pc.BLEND_NORMAL;
matOpaque.diffuse = new pc.Color(0.7, 0.05, 0.05);
matOpaque.specular = new pc.Color(0.01, 0.01, 0.01);
matOpaque.shininess = 90;
matOpaque.gloss = 1;
matOpaque.metalness = 0.0;
matOpaque.opacityMap = op;
matOpaque.opacityMapChannel = "r";
matOpaque.opacity = 1;
matOpaque.emissive = new pc.Color(0.372, 0.03, 0.003);
matOpaque.emissiveMap = ao;
matOpaque.emissiveIntensity = 1;
matOpaque.update();
//material for the tube
//transparent
tubeTransparent = new pc.StandardMaterial();
tubeTransparent.diffuse = new pc.Color(1, 1, 1);
tubeTransparent.blendType = pc.BLEND_NORMAL;
tubeTransparent.opacity = 0.1;
tubeTransparent.depthTest = false;
tubeTransparent.depthWrite = false;
tubeTransparent.useMetalness = true;
tubeTransparent.useDynamicRefraction = true;
tubeTransparent.thickness = 4;
tubeTransparent.update();
//opaque
tubeOpaque = new pc.StandardMaterial();
tubeOpaque.diffuse = new pc.Color(1, 1, 1);
tubeOpaque.opacity = 0.9;
tubeOpaque.update();
traverse(modelEntity, node => {
if (node.render && node.render.meshInstances) {
for (let mi of node.render.meshInstances) {
mi.material = matOpaque;
}
}
});
traverse(tubeEntity, node => {
if (node.render && node.render.meshInstances) {
for (let mi of node.render.meshInstances) {
mi.material = tubeOpaque;
}
}
});
// Do NOT override materials for bouchonEntity!
// 3. Position/scale/orientation
modelEntity.setPosition(modelX, modelY, modelZ);
modelEntity.setLocalScale(modelScale, modelScale, modelScale);
modelEntity.setLocalEulerAngles(modelRotationX, modelRotationY, modelRotationZ);
tubeEntity.setPosition(modelX, modelY, modelZ);
tubeEntity.setLocalScale(modelScale, modelScale, modelScale);
tubeEntity.setLocalEulerAngles(modelRotationX, modelRotationY, modelRotationZ);
bouchonEntity.setPosition(modelX, modelY, modelZ);
bouchonEntity.setLocalScale(modelScale, modelScale, modelScale);
bouchonEntity.setLocalEulerAngles(modelRotationX, modelRotationY, modelRotationZ);
// 4. Camera & orbit
cameraEntity = new pc.Entity('camera');
cameraEntity.addComponent('camera', {
clearColor: new pc.Color(1, 1, 1, 1),
toneMapping: pc.TONEMAP_NEUTRAL
});
cameraEntity.setPosition(chosenCameraX, chosenCameraY, chosenCameraZ);
cameraEntity.lookAt(modelEntity.getPosition());
cameraEntity.addComponent('script');
// --- **CRITICAL** for KHR_materials_transmission! ---
cameraEntity.camera.requestSceneColorMap(true);
cameraEntity.script.create('orbitCamera', {
attributes: {
focusEntity: modelEntity,
inertiaFactor: 0.2,
distanceMax: maxZoom,
distanceMin: minZoom,
pitchAngleMax: maxAngle,
pitchAngleMin: minAngle,
yawAngleMax: maxAzimuth,
yawAngleMin: minAzimuth,
minY: minY,
frameOnStart: false
}
});
cameraEntity.script.create('orbitCameraInputMouse');
cameraEntity.script.create('orbitCameraInputTouch');
app.root.addChild(cameraEntity);
// Remove the Skybox layer from the camera
const skyboxLayer = app.scene.layers.getLayerByName("Skybox");
const camLayers = cameraEntity.camera.layers.slice();
const idx = camLayers.indexOf(skyboxLayer.id);
if (idx !== -1) {
camLayers.splice(idx, 1);
cameraEntity.camera.layers = camLayers;
}
// --- Add this block for a cube parented to the camera ---
// Create the cube
const bgTex = assets.bg_tex.resource;
const bgPlane = new pc.Entity("Plane");
bgPlane.addComponent("model", { type: "plane" });
// Set the cube's local position relative to the camera
bgPlane.setLocalPosition(0, 0, -10); // 2 units in front of camera
// Optionally, scale or color the cube
bgPlane.setLocalScale(11, 1, 5.5);
bgPlane.setLocalEulerAngles(90, 0, 0);
// Simple material for visibility
const mat = new pc.StandardMaterial();
mat.diffuse = new pc.Color(1, 1, 1);
mat.diffuseMap = bgTex;
mat.emissive = new pc.Color(1, 1, 1);
mat.emissiveMap = bgTex;
mat.emissiveIntensity = 1;
mat.useLighting = false;
mat.update();
bgPlane.model.material = mat;
// Parent to the camera
cameraEntity.addChild(bgPlane);
// 5. Light
const light = new pc.Entity("mainLight");
light.addComponent('light',{
type: "directional",
color: new pc.Color(1, 1, 1),
intensity: 0.9,
});
light.setPosition(0, 1, -1);
light.lookAt(0, 0, 0);
app.root.addChild(light);
app.resizeCanvas(viewerContainer.clientWidth, viewerContainer.clientHeight);
app.once('update', () => resetViewerCamera());
// Tooltips supported if tooltips_url set
try {
if (config.tooltips_url) {
import('./tooltips.js').then(tooltipsModule => {
tooltipsModule.initializeTooltips({
app,
cameraEntity,
modelEntity,
tooltipsUrl: config.tooltips_url,
defaultVisible: !!config.showTooltipsDefault,
moveDuration: config.tooltipMoveDuration || 0.6
});
}).catch(e => {
// Tooltips optional: fail silently if missing
});
}
} catch (e) {
// Tooltips optional, fail silently
}
viewerInitialized = true;
});
});
app.assets.load(assets.helipad); // Start by loading the HDR
}
export function resetViewerCamera() {
try {
if (!cameraEntity || !modelEntity || !app) return;
const orbitCam = cameraEntity.script.orbitCamera;
if (!orbitCam) return;
const modelPos = modelEntity.getPosition();
const tempEnt = new pc.Entity();
tempEnt.setPosition(chosenCameraX, chosenCameraY, chosenCameraZ);
tempEnt.lookAt(modelPos);
const dist = new pc.Vec3().sub2(
new pc.Vec3(chosenCameraX, chosenCameraY, chosenCameraZ),
modelPos
).length();
cameraEntity.setPosition(chosenCameraX, chosenCameraY, chosenCameraZ);
cameraEntity.lookAt(modelPos);
orbitCam.pivotPoint = modelPos.clone();
orbitCam._targetDistance = dist;
orbitCam._distance = dist;
const rot = tempEnt.getRotation();
const fwd = new pc.Vec3();
rot.transformVector(pc.Vec3.FORWARD, fwd);
const yaw = Math.atan2(-fwd.x, -fwd.z) * pc.math.RAD_TO_DEG;
const yawQuat = new pc.Quat().setFromEulerAngles(0, -yaw, 0);
const rotNoYaw = new pc.Quat().mul2(yawQuat, rot);
const fNoYaw = new pc.Vec3();
rotNoYaw.transformVector(pc.Vec3.FORWARD, fNoYaw);
const pitch = Math.atan2(fNoYaw.y, -fNoYaw.z) * pc.math.RAD_TO_DEG;
orbitCam._targetYaw = yaw;
orbitCam._yaw = yaw;
orbitCam._targetPitch = pitch;
orbitCam._pitch = pitch;
if (orbitCam._updatePosition) orbitCam._updatePosition();
tempEnt.destroy();
} catch (e) {
// Silent fail
}
}
/**
* Change the main model's diffuse color in real time.
* r,g,b are floats [0,1]. This is a reusable function for new colors.
*/
export function changeColor(dr, dg, db, er, eg, eb, op, boolTrans) {
//cameraEntity.camera.clearColor = new pc.Color(colorBgR, colorBgG, colorBgB);
if(boolTrans){
if (!matTransparent) return;
matTransparent.diffuse.set(dr, dg, db);
matTransparent.emissive.set(er, eg, eb);
matTransparent.opacity = op;
matTransparent.update();
tubeTransparent.diffuse.set(dr, dg, db);
tubeTransparent.update();
traverse(modelEntity, node => {
if (node.render && node.render.meshInstances) {
for (let mi of node.render.meshInstances) {
mi.material = matTransparent;
}
}
});
traverse(tubeEntity, node => {
if (node.render && node.render.meshInstances) {
for (let mi of node.render.meshInstances) {
mi.material = tubeTransparent;
}
}
});
}else{
if (!matOpaque) return;
matOpaque.diffuse.set(dr, dg, db);
matOpaque.emissive.set(er, eg, eb);
matOpaque.update();
traverse(modelEntity, node => {
if (node.render && node.render.meshInstances) {
for (let mi of node.render.meshInstances) {
mi.material = matOpaque;
}
}
});
traverse(tubeEntity, node => {
if (node.render && node.render.meshInstances) {
for (let mi of node.render.meshInstances) {
mi.material = tubeOpaque;
}
}
});
}
}