From e894eeba682484fead54632e1cc8d159709f6ab8 Mon Sep 17 00:00:00 2001 From: Leon van Kammen Date: Sat, 18 Nov 2023 20:50:22 +0100 Subject: [PATCH] non-euclidian works now --- example/aframe/sandbox/index.html | 2 +- example/assets/AnimatedCube.bin | Bin 0 -> 1860 bytes example/assets/AnimatedCube.gltf | 262 + example/assets/AnimatedCube_NLA.gltf | 160 + example/assets/equirect.jpg | Bin 704028 -> 0 bytes example/assets/index.fbx | Bin 0 -> 5757420 bytes example/assets/index.glb | Bin 1770548 -> 1716720 bytes example/assets/index.mtl | 164 + example/assets/index.obj | 194201 +++++++++++++++++++ index.html | 10 +- src/3rd/js/three/xrf/dynamic/filter.js | 5 +- src/3rd/js/three/xrf/href.js | 6 +- src/3rd/js/three/xrf/src.js | 134 +- src/3rd/js/three/xrf/src/non-euclidian.js | 149 +- test/aframe/filter.js | 16 +- 15 files changed, 194978 insertions(+), 131 deletions(-) create mode 100644 example/assets/AnimatedCube.bin create mode 100644 example/assets/AnimatedCube.gltf create mode 100644 example/assets/AnimatedCube_NLA.gltf delete mode 100644 example/assets/equirect.jpg create mode 100644 example/assets/index.fbx create mode 100644 example/assets/index.mtl create mode 100644 example/assets/index.obj diff --git a/example/aframe/sandbox/index.html b/example/aframe/sandbox/index.html index 338e400..8f3130d 100644 --- a/example/aframe/sandbox/index.html +++ b/example/aframe/sandbox/index.html @@ -43,7 +43,7 @@ - + diff --git a/src/3rd/js/three/xrf/dynamic/filter.js b/src/3rd/js/three/xrf/dynamic/filter.js index 7eea46f..eec91b7 100644 --- a/src/3rd/js/three/xrf/dynamic/filter.js +++ b/src/3rd/js/three/xrf/dynamic/filter.js @@ -47,13 +47,14 @@ xrf.filter.process = function(frag,scene,opts){ String(m.userData['tag']).match( new RegExp("(^| )"+name_or_tag) ) const cleanupKey = (k) => k.replace(/[-\*]/g,'') - let firstFilter = frag.filters[0].filter.get() + let firstFilter = frag.filters.length ? frag.filters[0].filter.get() : false let showers = frag.filters.filter( (v) => v.filter.get().show === true ) // spec 2: https://xrfragment.org/doc/RFC_XR_Macros.html#embedding-xr-content-using-src // reparent scene based on objectname in case it matches a (non-negating) selector - if( opts.reparent && !firstFilter.value && firstFilter.show === true ){ + if( opts.reparent && firstFilter && !firstFilter.value && firstFilter.show === true ){ let obj + frag.target = firstFilter scene.traverse( (n) => hasName(n, firstFilter.key,firstFilter) && (obj = n) ) if(obj){ while( scene.children.length > 0 ) scene.children[0].removeFromParent() diff --git a/src/3rd/js/three/xrf/href.js b/src/3rd/js/three/xrf/href.js index 6c4c6c5..86f9c79 100644 --- a/src/3rd/js/three/xrf/href.js +++ b/src/3rd/js/three/xrf/href.js @@ -49,6 +49,7 @@ xrf.frag.href = function(v, opts){ } let selected = mesh.userData.XRF.href.selected = (state) => () => { + console.log("select "+mesh.name) if( mesh.selected == state ) return // nothing changed xrf.interactive.objects.map( (o) => { let newState = o.name == mesh.name ? state : false @@ -58,7 +59,7 @@ xrf.frag.href = function(v, opts){ if( o.material.emissive ){ if( !o.material.emissive.original ) o.material.emissive.original = o.material.emissive.clone() o.material.emissive.r = o.material.emissive.g = o.material.emissive.b = - newState ? o.material.emissive.original.r + 0.2 : o.material.emissive.original.r + newState ? o.material.emissive.original.r + 0.5 : o.material.emissive.original.r } } }) @@ -77,10 +78,11 @@ xrf.frag.href = function(v, opts){ mesh.addEventListener('mouseenter', selected(true) ) mesh.addEventListener('mouseleave', selected(false) ) + if( mesh.material ) mesh.material = mesh.material.clone() // clone, so we can individually highlight meshes + // lazy add mesh (because we're inside a recursive traverse) setTimeout( (mesh) => { xrf.interactive.add(mesh) - if( mesh.material ) mesh.material = mesh.material.clone() // clone, so we can individually highlight meshes xrf.emit('interactionReady', {mesh,xrf:v,clickHandler: mesh.userData.XRF.href.exec }) }, 0, mesh ) } diff --git a/src/3rd/js/three/xrf/src.js b/src/3rd/js/three/xrf/src.js index 09b36d3..3ae3b78 100644 --- a/src/3rd/js/three/xrf/src.js +++ b/src/3rd/js/three/xrf/src.js @@ -4,73 +4,83 @@ xrf.frag.src = function(v, opts){ opts.embedded = v // indicate embedded XR fragment let { mesh, model, camera, scene, renderer, THREE, hashbus, frag} = opts - const hasMaterialName = mesh.material && mesh.material.name.length > 0 + let url = v.string + let srcFrag = opts.srcFrag = xrfragment.URI.parse(url) + opts.isLocal = v.string[0] == '#' + + if( opts.isLocal ){ + xrf.frag.src.localSRC(url,srcFrag,opts) // local + }else xrf.frag.src.externalSRC(url,srcFrag,opts) // external file +} + +xrf.frag.src.addModel = (model,url,frag,opts) => { + let {mesh} = opts + let scene = model.scene + xrf.frag.src.filterScene(scene,{...opts,frag}) // filter scene + mesh.traverse( (n) => n.isSRC = n.isXRF = true ) // mark everything isSRC & isXRF + if( mesh.material ) mesh.material.visible = false // hide placeholder object + //enableSourcePortation(scene) + if( xrf.frag.src.renderAsPortal(mesh) ){ + if( !opts.isLocal ) xrf.scene.add(scene) + return xrf.portalNonEuclidian({...opts,model,scene:model.scene}) + }else{ + xrf.frag.src.scale( scene, opts, url ) // scale scene + mesh.add(scene) + } + xrf.emit('parseModel', {...opts, scene, model}) +} + +xrf.frag.src.renderAsPortal = (mesh) => { const hasTexture = mesh.material && mesh.material.map const isPlane = mesh.geometry && mesh.geometry.attributes.uv && mesh.geometry.attributes.uv.count == 4 - const hasLocalSRC = mesh.userData.src != undefined && mesh.userData.src[0] == '#' + const hasMaterialName = mesh.material && mesh.material.name.length > 0 + return mesh.geometry && !hasMaterialName && !hasTexture && isPlane +} - let src; - let url = v.string - let vfrag = xrfragment.URI.parse(url) +xrf.frag.src.enableSourcePortation = (src) => { + // show sourceportation clickable plane + if( srcFrag.href || v.string[0] == '#' ) return + let scale = new THREE.Vector3() + let size = new THREE.Vector3() + mesh.getWorldScale(scale) + new THREE.Box3().setFromObject(src).getSize(size) + const geo = new THREE.SphereGeometry( Math.max(size.x, size.y, size.z) / scale.x, 10, 10 ) + const mat = new THREE.MeshBasicMaterial() + mat.transparent = true + mat.roughness = 0.05 + mat.metalness = 1 + mat.opacity = 0 + const cube = new THREE.Mesh( geo, mat ) + console.log("todo: sourceportate") + return xrf.frag.src +} - // handle non-euclidian planes - if( mesh.geometry && !hasMaterialName && !hasTexture && hasLocalSRC && isPlane ){ - return xrf.portalNonEuclidian(opts) +xrf.frag.src.externalSRC = (url,frag,opts) => { + fetch(url, { method: 'HEAD' }) + .then( (res) => { + console.log(`loading src ${url}`) + let mimetype = res.headers.get('Content-type') + if( url.replace(/#.*/,'').match(/\.(gltf|glb)$/) ) mimetype = 'gltf' + //if( url.match(/\.(fbx|stl|obj)$/) ) mimetype = + opts = { ...opts, frag, mimetype } + return xrf.frag.src.type[ mimetype ] ? xrf.frag.src.type[ mimetype ](url,opts) : xrf.frag.src.type.unknown(url,opts) + }) + .then( (model) => { + if( model && model.scene ) xrf.frag.src.addModel(model, url, frag, opts ) + }) + .finally( () => { }) + .catch( console.error ) + return xrf.frag.src +} + +xrf.frag.src.localSRC = (url,frag,opts) => { + let {model,scene} = opts + let _model = { + animations: model.animations, + scene: scene.clone() } - - const addModel = (model,url,frag) => { - let scene = model.scene - xrf.frag.src.filterScene(scene,{...opts,frag}) - xrf.frag.src.scale( scene, opts, url ) - //enableSourcePortation(scene) - mesh.add(model.scene) - mesh.traverse( (n) => n.isSRC = n.isXRF = true ) // mark everything SRC - xrf.emit('parseModel', {...opts, scene, model}) - if( mesh.material ) mesh.material.visible = false // hide placeholder object - } - - const enableSourcePortation = (src) => { - // show sourceportation clickable plane - if( vfrag.href || v.string[0] == '#' ) return - let scale = new THREE.Vector3() - let size = new THREE.Vector3() - mesh.getWorldScale(scale) - new THREE.Box3().setFromObject(src).getSize(size) - const geo = new THREE.SphereGeometry( Math.max(size.x, size.y, size.z) / scale.x, 10, 10 ) - const mat = new THREE.MeshBasicMaterial() - mat.transparent = true - mat.roughness = 0.05 - mat.metalness = 1 - mat.opacity = 0 - const cube = new THREE.Mesh( geo, mat ) - console.log("todo: sourceportate") - } - - const externalSRC = (url,frag,src) => { - fetch(url, { method: 'HEAD' }) - .then( (res) => { - console.log(`loading src ${url}`) - let mimetype = res.headers.get('Content-type') - if( url.replace(/#.*/,'').match(/\.(gltf|glb)$/) ) mimetype = 'gltf' - //if( url.match(/\.(fbx|stl|obj)$/) ) mimetype = - opts = { ...opts, src, frag, mimetype } - return xrf.frag.src.type[ mimetype ] ? xrf.frag.src.type[ mimetype ](url,opts) : xrf.frag.src.type.unknown(url,opts) - }) - .then( (model) => { - if( model && model.scene ) addModel(model, url, frag ) - }) - .finally( () => { }) - .catch( console.error ) - } - - if( url[0] == "#" ){ - let _model = { - animations: model.animations, - scene: scene.clone() - } - _model.scenes = [_model.scene] - addModel(_model,url,vfrag) // current file - }else externalSRC(url,vfrag) // external file + _model.scenes = [_model.scene] + xrf.frag.src.addModel(_model,url,frag, opts) // current file } // scale embedded XR fragments https://xrfragment.org/#scaling%20of%20instanced%20objects diff --git a/src/3rd/js/three/xrf/src/non-euclidian.js b/src/3rd/js/three/xrf/src/non-euclidian.js index 36b7e96..fb3c41a 100644 --- a/src/3rd/js/three/xrf/src/non-euclidian.js +++ b/src/3rd/js/three/xrf/src/non-euclidian.js @@ -7,16 +7,15 @@ xrf.portalNonEuclidian = function(opts){ mesh.portal = { pos: mesh.position.clone(), posWorld: new xrf.THREE.Vector3(), + posWorldCamera: new xrf.THREE.Vector3(), stencilRef: xrf.portalNonEuclidian.stencilRef, - needUpdate: false + needUpdate: false, + stencilObject: false, + cameraDirection: new THREE.Vector3(), + cameraPosition: new THREE.Vector3(), + raycaster: new THREE.Raycaster() } - // turn mesh into stencilplane - xrf - .portalNonEuclidian - .setMaterial(mesh) - .getWorldPosition(mesh.portal.posWorld) - // allow objects to flip between original and stencil position (which puts them behind stencilplane) const addStencilFeature = (n) => { if( n.stencil ) return n // run once @@ -31,59 +30,106 @@ xrf.portalNonEuclidian = function(opts){ return n } - // collect related objects to render inside stencilplane - let stencilObject = scene.getObjectByName( mesh.userData.src.substr(1) ) // strip # - if( !stencilObject ) return console.warn(`no objects were found (src:${mesh.userData.src}) for (portal)object name '${mesh.name}'`) - let stencilObjects = [mesh,stencilObject] - stencilObjects = stencilObjects - .filter( (n) => !n.portal ) // filter out (self)references to portals (prevent recursion) - .map(addStencilFeature) + this.setupStencilObjects = (scene,opts) => { + // collect related objects to render inside stencilplane + let stencilObject = opts.srcFrag.target ? scene.getObjectByName( opts.srcFrag.target.key ) : scene // strip # + if( !stencilObject ) return console.warn(`no objects were found (src:${mesh.userData.src}) for (portal)object name '${mesh.name}'`) + if( !opts.isLocal ) stencilObject.visible = false + let stencilObjects = [mesh,stencilObject] + stencilObjects = stencilObjects + .filter( (n) => !n.portal ) // filter out (self)references to portals (prevent recursion) + .map(addStencilFeature) + mesh.portal.stencilObject = stencilObject - //// add missing lights to make sure things get lit properly - xrf.scene.traverse( (n) => n.isLight && - !stencilObjects.find( (o) => o.uuid == n.uuid ) && - stencilObjects.push(n) - ) + //// add missing lights to make sure things get lit properly + xrf.scene.traverse( (n) => n.isLight && + !stencilObjects.find( (o) => o.uuid == n.uuid ) && + stencilObjects.push(n) + ) - // put it into a scene (without .add() because it reparents objects) so we can render it separately - mesh.portal.stencilObjects = new xrf.THREE.Scene() - mesh.portal.stencilObjects.children = stencilObjects + // put it into a scene (without .add() because it reparents objects) so we can render it separately + mesh.portal.stencilObjects = new xrf.THREE.Scene() + mesh.portal.stencilObjects.children = stencilObjects + + xrf.portalNonEuclidian.stencilRef += 1 // each portal has unique stencil id + console.log(`enabling portal for object '${mesh.name}' (stencilRef:${mesh.portal.stencilRef})`) + + // clone so it won't be affected by other fragments + setTimeout( (mesh) => { + if( mesh.material ) mesh.material = mesh.material.clone() // clone, so we can individually highlight meshes + }, 0, mesh ) + + return this + } // enable the stencil-material of the stencil objects to prevent stackoverflow (portal in portal rendering) const showPortal = (n,show) => { if( n.portal ) n.visible = show return true } - - mesh.onBeforeRender = function(renderer, scene, camera, geometry, material, group ){ - mesh.visible = false - } - mesh.onAfterRender = function(renderer, scene, camera, geometry, material, group ){ - mesh.portal.needUpdate = true - } + this.setupListeners = () => { - xrf.addEventListener('renderPost', (opts) => { - if( mesh.portal && mesh.portal.needUpdate ){ - let {scene,camera,time,render} = opts - let stencilRef = mesh.portal.stencilRef - let newPos = mesh.portal.posWorld - let newScale = mesh.scale - - mesh.visible = true - - mesh.portal.stencilObjects.traverse( (n) => showPortal(n,false) && n.stencil && n.stencil(stencilRef,newPos,newScale) ) - renderer.autoClear = false - renderer.clearDepth() - render( mesh.portal.stencilObjects, camera ) - mesh.portal.stencilObjects.traverse( (n) => showPortal(n,true) && n.stencil && (n.stencil(0)) ) - - mesh.portal.needUpdate = false + mesh.onBeforeRender = function(renderer, scene, camera, geometry, material, group ){ } - }) - xrf.portalNonEuclidian.stencilRef += 1 // each portal has unique stencil id - console.log(`enabling portal for object '${mesh.name}' (stencilRef:${mesh.portal.stencilRef})`) + mesh.onAfterRender = function(renderer, scene, camera, geometry, material, group ){ + mesh.portal.needUpdate = true + } + + xrf.addEventListener('renderPost', (opts) => { + if( mesh.portal && mesh.portal.needUpdate && mesh.portal.stencilObjects ){ + let {scene,camera,time,render} = opts + let stencilRef = mesh.portal.stencilRef + let newPos = mesh.portal.posWorld + let stencilObject = mesh.portal.stencilObject + let newScale = mesh.scale + let cameraDirection = mesh.portal.cameraDirection + let cameraPosition = mesh.portal.cameraPosition + let raycaster = mesh.portal.raycaster + + // init + if( !opts.isLocal ) stencilObject.visible = true + mesh.portal.stencilObjects.traverse( (n) => showPortal(n,false) && n.stencil && n.stencil(stencilRef,newPos,newScale) ) + renderer.autoClear = false + renderer.clearDepth() + // render + render( mesh.portal.stencilObjects, camera ) + // de-init + mesh.portal.stencilObjects.traverse( (n) => showPortal(n,true) && n.stencil && (n.stencil(0)) ) + if( !opts.isLocal ) stencilObject.visible = false + + + // trigger href upon camera collide + if( mesh.userData.XRF.href ){ + raycaster.far = 0.3 + let cam = xrf.camera.getCam ? xrf.camera.getCam() : camera + cam.getWorldPosition(cameraPosition) + cam.getWorldDirection(cameraDirection) + raycaster.set(cameraPosition, cameraDirection ) + intersects = raycaster.intersectObjects([mesh], false) + if (intersects.length > 0 && !mesh.portal.teleporting ){ + mesh.portal.teleporting = true + mesh.userData.XRF.href.exec() + setTimeout( () => mesh.portal.teleporting = false, 500) // dont flip back and forth + } + } + + mesh.portal.needUpdate = false + } + }) + return this + } + + // turn mesh into stencilplane + xrf + .portalNonEuclidian + .setMaterial(mesh) + .getWorldPosition(mesh.portal.posWorld) + + this + .setupListeners() + .setupStencilObjects(scene,opts) } @@ -98,15 +144,16 @@ xrf.portalNonEuclidian.selectStencil = (n, stencilRef, nested) => { xrf.portalNonEuclidian.setMaterial = function(mesh){ mesh.material = new xrf.THREE.MeshBasicMaterial({ color: 'white' }); - mesh.material.depthWrite = true; + mesh.material.depthWrite = false; mesh.material.depthTest = false; mesh.material.colorWrite = false; mesh.material.stencilWrite = true; mesh.material.stencilRef = xrf.portalNonEuclidian.stencilRef; + mesh.renderOrder = xrf.portalNonEuclidian.stencilRef; mesh.material.stencilFunc = THREE.AlwaysStencilFunc; mesh.material.stencilZPass = THREE.ReplaceStencilOp; - mesh.material.stencilFail = THREE.ReplaceStencilOp; - mesh.material.stencilZFail = THREE.ReplaceStencilOp; + //mesh.material.stencilFail = THREE.ReplaceStencilOp; + //mesh.material.stencilZFail = THREE.ReplaceStencilOp; return mesh } diff --git a/test/aframe/filter.js b/test/aframe/filter.js index 3aaea54..61e0c7c 100644 --- a/test/aframe/filter.js +++ b/test/aframe/filter.js @@ -53,7 +53,7 @@ test = () => scn.visible("a",true) && scn.visible("b",false) && scn.visible("c" console.assert( test(), {scn,reason:`objectname: #a&-b `}) scn = filterScene("#-b&b") -test = () => scn.visible("a",false) && scn.visible("b",true) && scn.visible("c",true) +test = () => scn.visible("a",true) && scn.visible("b",true) && scn.visible("c",true) console.assert( test(), {scn,reason:`objectname: #-b&b `}) scn = filterScene("#-c") @@ -66,23 +66,23 @@ console.assert( test(), {scn,reason:`propertyfilter: #score `}) scn = filterScene("#score=>1") test = () => scn.visible("a",true) && scn.visible("b",true) && scn.visible("c",true) -console.assert( test(), {scn,reason:`propertyfilter: #score`}) +console.assert( test(), {scn,reason:`propertyfilter: #score>=1`}) scn = filterScene("#score=2") test = () => scn.visible("a",true) && scn.visible("b",true) && scn.visible("c",true) -console.assert( test(), {scn,reason:`propertyfilter: #score`}) +console.assert( test(), {scn,reason:`propertyfilter: #score=2`}) scn = filterScene("#score=>3") test = () => scn.visible("a",true) && scn.visible("b",false) && scn.visible("c",false) -console.assert( test(), {scn,reason:`propertyfilter: #score`}) +console.assert( test(), {scn,reason:`propertyfilter: #score=>3`}) scn = filterScene("#-score=>1") test = () => scn.visible("a",true) && scn.visible("b",false) && scn.visible("c",false) -console.assert( test(), {scn,reason:`propertyfilter: #-score`}) +console.assert( test(), {scn,reason:`propertyfilter: #-score=>1`}) scn = filterScene("#-score=>1&c") test = () => scn.visible("a",true) && scn.visible("b",true) && scn.visible("b",false,true) && scn.visible("c",true) -console.assert( test(), {scn,reason:`propertyfilter: #-score`}) +console.assert( test(), {scn,reason:`propertyfilter: #-score=>1&c`}) scn = filterScene("#-foo") test = () => scn.visible("a",true) && scn.visible("b",false) && scn.visible("b",false) @@ -100,6 +100,6 @@ scn = filterScene("#-b&-foo&bar&flop&-bar&flop") test = () => scn.visible("a",true) && scn.visible("b",false,true) && scn.visible("c",true) console.assert( test(), {scn,reason:`tagfilter: #-b&-foo&bar&flop&-bar&flop"`}) -scn = filterScene("#-price&price=>4") +scn = filterScene("#-price&price=>5") test = () => scn.visible("a",false,true) && scn.visible("b",true) && scn.visible("c",true) -console.assert( test(), {scn,reason:`tagfilter: #-price&price=>4"`}) +console.assert( test(), {scn,reason:`tagfilter: #-price&price=>5"`})