Update viewer_ar.js
Browse files- viewer_ar.js +81 -7
viewer_ar.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
| 3 |
- Slider custom Yaw (360° en haut → 0° en bas), knob centré, rail plein
|
| 4 |
- Blocage total des interactions scène quand on touche le slider
|
| 5 |
- Éclairage PBR par défaut (sans WebXR light estimation)
|
|
|
|
| 6 |
*/
|
| 7 |
|
| 8 |
(() => {
|
|
@@ -70,7 +71,7 @@
|
|
| 70 |
}
|
| 71 |
|
| 72 |
// ===== Boot =====
|
| 73 |
-
(async()=>{ try{ await loadPlayCanvasRobust({esmFirst:true,loadTimeoutMs
|
| 74 |
|
| 75 |
// ===== App =====
|
| 76 |
function initARApp(){
|
|
@@ -96,7 +97,7 @@
|
|
| 96 |
const onResize = () => app.resizeCanvas(); window.addEventListener("resize", onResize); app.on("destroy",()=>window.removeEventListener("resize",onResize));
|
| 97 |
app.start();
|
| 98 |
|
| 99 |
-
// ===== Rendu / PBR defaults
|
| 100 |
app.scene.gammaCorrection = pc.GAMMA_SRGB;
|
| 101 |
app.scene.toneMapping = pc.TONEMAP_ACES;
|
| 102 |
app.scene.exposure = 1; // ajuste 0.9–1.8 si besoin
|
|
@@ -109,7 +110,7 @@
|
|
| 109 |
|
| 110 |
const light = new pc.Entity("Light");
|
| 111 |
light.addComponent("light", { type: "directional", intensity: 1.0, castShadows: true, color: new pc.Color(1,1,1) });
|
| 112 |
-
light.setLocalEulerAngles(45, 30, 0);
|
| 113 |
app.root.addChild(light);
|
| 114 |
|
| 115 |
// Réticule
|
|
@@ -120,6 +121,53 @@
|
|
| 120 |
const modelRoot = new pc.Entity("ModelRoot"); modelRoot.enabled = false; app.root.addChild(modelRoot);
|
| 121 |
let modelLoaded=false, placedOnce=false;
|
| 122 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 123 |
// Euler de base (évite inversions)
|
| 124 |
let baseEulerX=0, baseEulerZ=0;
|
| 125 |
|
|
@@ -140,6 +188,16 @@
|
|
| 140 |
updateKnobFromY(y);
|
| 141 |
}
|
| 142 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 143 |
// Chargement GLB + "fix matériaux"
|
| 144 |
app.assets.loadFromUrl(GLB_URL, "container", (err, asset) => {
|
| 145 |
if (err){ console.error(err); message("Échec du chargement du modèle GLB."); return; }
|
|
@@ -147,16 +205,17 @@
|
|
| 147 |
modelRoot.addChild(instance);
|
| 148 |
modelRoot.setLocalScale(1,1,1);
|
| 149 |
|
| 150 |
-
// Fix matériaux (
|
| 151 |
const renders = instance.findComponents('render');
|
| 152 |
for (const r of renders) {
|
|
|
|
| 153 |
for (const mi of r.meshInstances) {
|
| 154 |
const m = mi.material;
|
| 155 |
if (!m) continue;
|
| 156 |
if (m.diffuse && (m.diffuse.r !== 1 || m.diffuse.g !== 1 || m.diffuse.b !== 1)) {
|
| 157 |
m.diffuse.set(1,1,1);
|
| 158 |
}
|
| 159 |
-
if ('useSkybox' in m) m.useSkybox = true;
|
| 160 |
m.update();
|
| 161 |
}
|
| 162 |
}
|
|
@@ -221,7 +280,6 @@
|
|
| 221 |
app.xr.domOverlay.root = document.getElementById("xr-overlay-root");
|
| 222 |
camera.camera.startXr(pc.XRTYPE_AR, pc.XRSPACE_LOCALFLOOR, {
|
| 223 |
requiredFeatures: ["hit-test","dom-overlay"],
|
| 224 |
-
// PAS de "light-estimation" ici (demandé)
|
| 225 |
domOverlay: { root: app.xr.domOverlay.root },
|
| 226 |
callback: (err) => { if (err){ console.error("Échec du démarrage AR :", err); message(`Échec du démarrage AR : ${err.message||err}`); } }
|
| 227 |
});
|
|
@@ -253,6 +311,9 @@
|
|
| 253 |
modelRoot.enabled = true;
|
| 254 |
modelRoot.setPosition(pos);
|
| 255 |
|
|
|
|
|
|
|
|
|
|
| 256 |
const e = new pc.Vec3(); rot.getEulerAngles(e);
|
| 257 |
// normalise yaw initiale 0..360
|
| 258 |
const y0 = ((e.y % 360)+360)%360;
|
|
@@ -284,6 +345,7 @@
|
|
| 284 |
if (!isDragging) return;
|
| 285 |
if (!isHorizontalUpFacing(rot)) return;
|
| 286 |
modelRoot.setPosition(pos);
|
|
|
|
| 287 |
});
|
| 288 |
|
| 289 |
transientSource.once("remove", () => { isDragging = false; });
|
|
@@ -296,7 +358,19 @@
|
|
| 296 |
// Desktop : rotation souris conservée (ignore si UI)
|
| 297 |
let rotateMode=false, lastMouseX=0; const ROTATE_SENSITIVITY=0.25;
|
| 298 |
app.mouse.on("mousedown",(e)=>{ if(!app.xr.active||!placedOnce||uiInteracting) return; if(e.button===0 && !e.shiftKey) isDragging=true; else if(e.button===2 || (e.button===0 && e.shiftKey)){ rotateMode=true; lastMouseX=e.x; }});
|
| 299 |
-
app.mouse.on("mousemove",(e)=>{
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 300 |
app.mouse.on("mouseup",()=>{ isDragging=false; rotateMode=false; });
|
| 301 |
window.addEventListener("contextmenu",(e)=>e.preventDefault());
|
| 302 |
|
|
|
|
| 3 |
- Slider custom Yaw (360° en haut → 0° en bas), knob centré, rail plein
|
| 4 |
- Blocage total des interactions scène quand on touche le slider
|
| 5 |
- Éclairage PBR par défaut (sans WebXR light estimation)
|
| 6 |
+
- Blob Shadow (ombre de contact) sous l’objet
|
| 7 |
*/
|
| 8 |
|
| 9 |
(() => {
|
|
|
|
| 71 |
}
|
| 72 |
|
| 73 |
// ===== Boot =====
|
| 74 |
+
(async()=>{ try{ await loadPlayCanvasRobust({esmFirst:true,loadTimeoutMs=15000}); }catch(e){ console.error("Chargement PlayCanvas échoué ->",e); message("Impossible de charger PlayCanvas (réseau/CDN). Réessaie plus tard."); return; } initARApp(); })();
|
| 75 |
|
| 76 |
// ===== App =====
|
| 77 |
function initARApp(){
|
|
|
|
| 97 |
const onResize = () => app.resizeCanvas(); window.addEventListener("resize", onResize); app.on("destroy",()=>window.removeEventListener("resize",onResize));
|
| 98 |
app.start();
|
| 99 |
|
| 100 |
+
// ===== Rendu / PBR defaults =====
|
| 101 |
app.scene.gammaCorrection = pc.GAMMA_SRGB;
|
| 102 |
app.scene.toneMapping = pc.TONEMAP_ACES;
|
| 103 |
app.scene.exposure = 1; // ajuste 0.9–1.8 si besoin
|
|
|
|
| 110 |
|
| 111 |
const light = new pc.Entity("Light");
|
| 112 |
light.addComponent("light", { type: "directional", intensity: 1.0, castShadows: true, color: new pc.Color(1,1,1) });
|
| 113 |
+
light.setLocalEulerAngles(45, 30, 0);
|
| 114 |
app.root.addChild(light);
|
| 115 |
|
| 116 |
// Réticule
|
|
|
|
| 121 |
const modelRoot = new pc.Entity("ModelRoot"); modelRoot.enabled = false; app.root.addChild(modelRoot);
|
| 122 |
let modelLoaded=false, placedOnce=false;
|
| 123 |
|
| 124 |
+
// ===== Blob Shadow =====
|
| 125 |
+
let blob = null; // entité plane de l’ombre
|
| 126 |
+
const BLOB_SIZE = 0.7; // diamètre approx. de l’ombre (m)
|
| 127 |
+
const BLOB_OFFSET_Y = 0.005; // petit décalage au-dessus du sol pour éviter le z-fighting
|
| 128 |
+
|
| 129 |
+
function makeBlobTexture(app, size = 256) {
|
| 130 |
+
const cvs = document.createElement('canvas');
|
| 131 |
+
cvs.width = cvs.height = size;
|
| 132 |
+
const ctx = cvs.getContext('2d');
|
| 133 |
+
const r = size * 0.45;
|
| 134 |
+
const grd = ctx.createRadialGradient(size/2, size/2, r*0.2, size/2, size/2, r);
|
| 135 |
+
grd.addColorStop(0, 'rgba(0,0,0,0.35)'); // centre sombre
|
| 136 |
+
grd.addColorStop(1, 'rgba(0,0,0,0.0)'); // bords transparents
|
| 137 |
+
ctx.fillStyle = grd;
|
| 138 |
+
ctx.fillRect(0, 0, size, size);
|
| 139 |
+
|
| 140 |
+
const tex = new pc.Texture(app.graphicsDevice, {
|
| 141 |
+
width: size, height: size, format: pc.PIXELFORMAT_R8_G8_B8_A8,
|
| 142 |
+
mipmaps: true, magFilter: pc.FILTER_LINEAR, minFilter: pc.FILTER_LINEAR_MIPMAP_LINEAR
|
| 143 |
+
});
|
| 144 |
+
tex.setSource(cvs);
|
| 145 |
+
return tex;
|
| 146 |
+
}
|
| 147 |
+
|
| 148 |
+
function createBlobShadowAt(pos, rot) {
|
| 149 |
+
const tex = makeBlobTexture(app, 256);
|
| 150 |
+
const blobMat = new pc.StandardMaterial();
|
| 151 |
+
blobMat.diffuse.set(0,0,0);
|
| 152 |
+
blobMat.opacity = 1.0; // la transparence vient de la texture
|
| 153 |
+
blobMat.blendType = pc.BLEND_PREMULTIPLIED;
|
| 154 |
+
blobMat.depthWrite = false; // pour ne pas masquer l'objet
|
| 155 |
+
blobMat.diffuseMap = tex;
|
| 156 |
+
blobMat.update();
|
| 157 |
+
|
| 158 |
+
const e = new pc.Entity("BlobShadow");
|
| 159 |
+
e.addComponent("render", { type: "plane", castShadows: false, receiveShadows: false });
|
| 160 |
+
e.render.material = blobMat;
|
| 161 |
+
|
| 162 |
+
// place légèrement au-dessus du sol, orienté comme le plan détecté (horizontal)
|
| 163 |
+
e.setPosition(pos.x, pos.y + BLOB_OFFSET_Y, pos.z);
|
| 164 |
+
e.setRotation(rot);
|
| 165 |
+
e.setLocalScale(BLOB_SIZE, 1, BLOB_SIZE);
|
| 166 |
+
|
| 167 |
+
app.root.addChild(e);
|
| 168 |
+
return e;
|
| 169 |
+
}
|
| 170 |
+
|
| 171 |
// Euler de base (évite inversions)
|
| 172 |
let baseEulerX=0, baseEulerZ=0;
|
| 173 |
|
|
|
|
| 188 |
updateKnobFromY(y);
|
| 189 |
}
|
| 190 |
|
| 191 |
+
// Helper : met à jour aussi l’ombre quand on déplace l’objet
|
| 192 |
+
function updateBlobPositionUnder(pos, rotLikePlane = null) {
|
| 193 |
+
if (!blob) return;
|
| 194 |
+
// on suit la position XY du modèle, et on s'aligne sur la rotation du plan si fournie
|
| 195 |
+
blob.setPosition(pos.x, pos.y + BLOB_OFFSET_Y, pos.z);
|
| 196 |
+
if (rotLikePlane) {
|
| 197 |
+
blob.setRotation(rotLikePlane);
|
| 198 |
+
}
|
| 199 |
+
}
|
| 200 |
+
|
| 201 |
// Chargement GLB + "fix matériaux"
|
| 202 |
app.assets.loadFromUrl(GLB_URL, "container", (err, asset) => {
|
| 203 |
if (err){ console.error(err); message("Échec du chargement du modèle GLB."); return; }
|
|
|
|
| 205 |
modelRoot.addChild(instance);
|
| 206 |
modelRoot.setLocalScale(1,1,1);
|
| 207 |
|
| 208 |
+
// Fix matériaux (au cas où) : s’assure d’un rendu PBR correct
|
| 209 |
const renders = instance.findComponents('render');
|
| 210 |
for (const r of renders) {
|
| 211 |
+
r.castShadows = true; // l’objet projette des ombres (utile si tu actives de vraies ombres)
|
| 212 |
for (const mi of r.meshInstances) {
|
| 213 |
const m = mi.material;
|
| 214 |
if (!m) continue;
|
| 215 |
if (m.diffuse && (m.diffuse.r !== 1 || m.diffuse.g !== 1 || m.diffuse.b !== 1)) {
|
| 216 |
m.diffuse.set(1,1,1);
|
| 217 |
}
|
| 218 |
+
if ('useSkybox' in m) m.useSkybox = true;
|
| 219 |
m.update();
|
| 220 |
}
|
| 221 |
}
|
|
|
|
| 280 |
app.xr.domOverlay.root = document.getElementById("xr-overlay-root");
|
| 281 |
camera.camera.startXr(pc.XRTYPE_AR, pc.XRSPACE_LOCALFLOOR, {
|
| 282 |
requiredFeatures: ["hit-test","dom-overlay"],
|
|
|
|
| 283 |
domOverlay: { root: app.xr.domOverlay.root },
|
| 284 |
callback: (err) => { if (err){ console.error("Échec du démarrage AR :", err); message(`Échec du démarrage AR : ${err.message||err}`); } }
|
| 285 |
});
|
|
|
|
| 311 |
modelRoot.enabled = true;
|
| 312 |
modelRoot.setPosition(pos);
|
| 313 |
|
| 314 |
+
// Crée l’ombre de contact au même endroit/orientation
|
| 315 |
+
blob = createBlobShadowAt(pos, rot);
|
| 316 |
+
|
| 317 |
const e = new pc.Vec3(); rot.getEulerAngles(e);
|
| 318 |
// normalise yaw initiale 0..360
|
| 319 |
const y0 = ((e.y % 360)+360)%360;
|
|
|
|
| 345 |
if (!isDragging) return;
|
| 346 |
if (!isHorizontalUpFacing(rot)) return;
|
| 347 |
modelRoot.setPosition(pos);
|
| 348 |
+
updateBlobPositionUnder(pos, rot); // <— l’ombre suit le modèle
|
| 349 |
});
|
| 350 |
|
| 351 |
transientSource.once("remove", () => { isDragging = false; });
|
|
|
|
| 358 |
// Desktop : rotation souris conservée (ignore si UI)
|
| 359 |
let rotateMode=false, lastMouseX=0; const ROTATE_SENSITIVITY=0.25;
|
| 360 |
app.mouse.on("mousedown",(e)=>{ if(!app.xr.active||!placedOnce||uiInteracting) return; if(e.button===0 && !e.shiftKey) isDragging=true; else if(e.button===2 || (e.button===0 && e.shiftKey)){ rotateMode=true; lastMouseX=e.x; }});
|
| 361 |
+
app.mouse.on("mousemove",(e)=>{
|
| 362 |
+
if(!app.xr.active||!placedOnce||uiInteracting) return;
|
| 363 |
+
if(isDragging){
|
| 364 |
+
if(reticle.enabled) {
|
| 365 |
+
const p = reticle.getPosition();
|
| 366 |
+
modelRoot.setPosition(p);
|
| 367 |
+
updateBlobPositionUnder(p); // <— suit aussi en drag souris via réticule
|
| 368 |
+
}
|
| 369 |
+
} else if(rotateMode && modelRoot.enabled){
|
| 370 |
+
const dx=e.x-lastMouseX; lastMouseX=e.x;
|
| 371 |
+
applyRotationY(rotationYDeg + dx*ROTATE_SENSITIVITY);
|
| 372 |
+
}
|
| 373 |
+
});
|
| 374 |
app.mouse.on("mouseup",()=>{ isDragging=false; rotateMode=false; });
|
| 375 |
window.addEventListener("contextmenu",(e)=>e.preventDefault());
|
| 376 |
|