From 81e9aca0753fe99c04dd69cb150993810e207461 Mon Sep 17 00:00:00 2001
From: Leon van Kammen
Date: Wed, 17 May 2023 21:31:28 +0200
Subject: [PATCH] work in progress [might break]
---
dist/xrfragment.aframe.js | 221 +++++++++++++++++++----------
dist/xrfragment.three.js | 187 +++++++++++++++---------
example/aframe/sandbox/index.html | 19 +--
example/assets/example3.gltf | 150 +++++++++++++-------
example/assets/style.css | 22 +--
example/assets/utils.js | 16 ++-
example/threejs/sandbox/index.html | 10 +-
example/threejs/sandbox/main.css | 91 ------------
src/3rd/aframe/index.js | 34 +++--
src/3rd/three/InteractiveGroup.js | 11 +-
src/3rd/three/index.js | 87 +++++-------
src/3rd/three/navigator.js | 42 ++++++
src/3rd/three/xrf/href.js | 47 +++---
src/3rd/three/xrf/src.js | 42 ++++--
src/3rd/wasm/README.md | 2 +
src/3rd/wasm/index.js | 50 +++++++
src/3rd/wasm/index.wasm | Bin 0 -> 895562 bytes
17 files changed, 612 insertions(+), 419 deletions(-)
delete mode 100644 example/threejs/sandbox/main.css
create mode 100644 src/3rd/three/navigator.js
create mode 100644 src/3rd/wasm/README.md
create mode 100644 src/3rd/wasm/index.js
create mode 100644 src/3rd/wasm/index.wasm
diff --git a/dist/xrfragment.aframe.js b/dist/xrfragment.aframe.js
index 82d6634..77ab1fb 100644
--- a/dist/xrfragment.aframe.js
+++ b/dist/xrfragment.aframe.js
@@ -620,6 +620,13 @@ xrfragment.InteractiveGroup = function(THREE,renderer,camera){
const raycaster = new Raycaster();
const tempMatrix = new Matrix4();
+ function nocollide(){
+ if( nocollide.tid ) return // ratelimit
+ _event.type = "nocollide"
+ scope.children.map( (c) => c.dispatchEvent(_event) )
+ nocollide.tid = setTimeout( () => nocollide.tid = null, 100 )
+ }
+
// Pointer Events
const element = renderer.domElement;
@@ -649,7 +656,7 @@ xrfragment.InteractiveGroup = function(THREE,renderer,camera){
object.dispatchEvent( _event );
- }
+ }else nocollide()
}
@@ -697,7 +704,7 @@ xrfragment.InteractiveGroup = function(THREE,renderer,camera){
object.dispatchEvent( _event );
- }
+ }else nocollide()
}
@@ -721,7 +728,7 @@ xrfragment.InteractiveGroup = function(THREE,renderer,camera){
}
let xrf = xrfragment
xrf.frag = {}
-xrf.model = {}
+xrf.model = {}
xrf.init = function(opts){
opts = opts || {}
@@ -732,8 +739,10 @@ xrf.init = function(opts){
for ( let i in xrf.XRF ) xrf.XRF[i] // shortcuts to constants (NAVIGATOR e.g.)
xrf.Parser.debug = xrf.debug
if( opts.loaders ) Object.values(opts.loaders).map( xrf.patchLoader )
+
+ xrf.interactive = xrf.InteractiveGroup( opts.THREE, opts.renderer, opts.camera)
+ xrf.scene.add( xrf.interactive)
xrf.patchRenderer(opts.renderer)
- xrf.navigate.init()
return xrf
}
@@ -763,15 +772,7 @@ xrf.parseModel = function(model,url){
let file = xrf.getFile(url)
model.file = file
model.render = function(){}
- model.interactive = xrf.InteractiveGroup( xrf.THREE, xrf.renderer, xrf.camera)
- model.scene.add(model.interactive)
-
- console.log("scanning "+file)
-
- model.scene.traverse( (mesh) => {
- console.log("◎ "+ (mesh.name||`THREE.${mesh.constructor.name}`))
- xrf.eval.mesh(mesh,model)
- })
+ model.scene.traverse( (mesh) => xrf.eval.mesh(mesh,model) )
}
xrf.getLastModel = () => xrf.model.last
@@ -800,6 +801,7 @@ xrf.eval.mesh = (mesh,model) => {
for( let k in mesh.userData ) xrf.Parser.parse( k, mesh.userData[k], frag )
for( let k in frag ){
let opts = {frag, mesh, model, camera: xrf.camera, scene: xrf.scene, renderer: xrf.renderer, THREE: xrf.THREE }
+ mesh.userData.XRF = frag // allow fragment impl to access XRF obj already
xrf.eval.fragment(k,opts)
}
}
@@ -813,52 +815,80 @@ xrf.eval.fragment = (k, opts ) => {
}
xrf.reset = () => {
- if( !xrf.model.scene ) return
- xrf.scene.remove( xrf.model.scene )
- xrf.model.scene.traverse( function(node){
- if( node instanceof xrf.THREE.Mesh ){
- node.geometry.dispose()
- node.material.dispose()
+ console.log("xrf.reset()")
+
+ const disposeObject = (obj) => {
+ if (obj.children.length > 0) obj.children.forEach((child) => disposeObject(child));
+ if (obj.geometry) obj.geometry.dispose();
+ if (obj.material) {
+ if (obj.material.map) obj.material.map.dispose();
+ obj.material.dispose();
}
+ return true
+ };
+
+ for ( let i in xrf.scene.children ) {
+ const child = xrf.scene.children[i];
+ if( child.xrf ){ // dont affect user objects
+ disposeObject(child);
+ xrf.scene.remove(child);
+ }
+ }
+ // remove interactive xrf objs like href-portals
+ xrf.interactive.traverse( (n) => {
+ if( disposeObject(n) ) xrf.interactive.remove(n)
})
}
-xrf.navigate = {}
+xrf.parseUrl = (url) => {
+ const urlObj = new URL( url.match(/:\/\//) ? url : String(`https://fake.com/${url}`).replace(/\/\//,'/') )
+ let dir = url.substring(0, url.lastIndexOf('/') + 1)
+ const file = urlObj.pathname.substring(urlObj.pathname.lastIndexOf('/') + 1);
+ const hash = url.match(/#/) ? url.replace(/.*#/,'') : ''
+ const ext = file.split('.').pop()
+ return {urlObj,dir,file,hash,ext}
+}
+xrf.navigator = {}
-xrf.navigate.to = (url) => {
+xrf.navigator.to = (url) => {
return new Promise( (resolve,reject) => {
console.log("xrfragment: navigating to "+url)
+ let {urlObj,dir,file,hash,ext} = xrf.parseUrl(url)
if( xrf.model && xrf.model.scene ) xrf.model.scene.visible = false
- const urlObj = new URL( url.match(/:\/\//) ? url : String(`https://fake.com/${url}`).replace(/\/\//,'/') )
- let dir = url.substring(0, url.lastIndexOf('/') + 1)
- const file = urlObj.pathname.substring(urlObj.pathname.lastIndexOf('/') + 1);
- const ext = file.split('.').pop()
+ console.log("ext="+ext)
const Loader = xrf.loaders[ext]
if( !Loader ) throw 'xrfragment: no loader passed to xrfragment for extension .'+ext
// force relative path
if( dir ) dir = dir[0] == '.' ? dir : `.${dir}`
const loader = new Loader().setPath( dir )
loader.load( file, function(model){
- xrf.scene.add( model.scene )
xrf.reset()
+ model.scene.xrf = true // leave mark for reset()
+ xrf.scene.add( model.scene )
xrf.model = model
- xrf.navigate.commit( file )
+ xrf.navigator.commit( file, hash )
resolve(model)
})
})
}
-xrf.navigate.init = () => {
- if( xrf.navigate.init.inited ) return
+xrf.navigator.init = () => {
+ if( xrf.navigator.init.inited ) return
window.addEventListener('popstate', function (event){
- console.dir(event)
- xrf.navigate.to( document.location.search.substr(1) + document.location.hash )
+ console.log(event.target.document.location.search)
+ console.log(event.currentTarget.document.location.search)
+ console.log(document.location.search)
+ xrf.navigator.to( document.location.search.substr(1) + document.location.hash )
})
- xrf.navigate.init.inited = true
+ let {url,urlObj,dir,file,hash,ext} = xrf.parseUrl(document.location.href)
+ //console.dir({file,hash})
+ xrf.navigator.commit(file,document.location.hash)
+ xrf.navigator.init.inited = true
}
-xrf.navigate.commit = (file) => {
- window.history.pushState({},null, document.location.pathname + `?${file}${document.location.hash}` )
+xrf.navigator.commit = (file,hash) => {
+ console.log("hash="+hash)
+ window.history.pushState({},null, document.location.pathname + `?${file}#${hash}` )
}
xrf.frag.env = function(v, opts){
let { mesh, model, camera, scene, renderer, THREE} = opts
@@ -898,16 +928,27 @@ xrf.frag.env = function(v, opts){
xrf.frag.href = function(v, opts){
let { mesh, model, camera, scene, renderer, THREE} = opts
+ const world = { pos: new THREE.Vector3(), scale: new THREE.Vector3() }
+ mesh.getWorldPosition(world.pos)
+ mesh.getWorldScale(world.scale)
+ mesh.position.copy(world.pos)
+ mesh.scale.copy(world.scale)
+ console.log("HREF: "+(model.recursive ?"src-instanced":"original"))
+
+ // convert texture if needed
let texture = mesh.material.map
- texture.mapping = THREE.ClampToEdgeWrapping
- texture.needsUpdate = true
- mesh.material.dispose()
+ if( texture && texture.source.data.height == texture.source.data.width/2 ){
+ // assume equirectangular image
+ texture.mapping = THREE.ClampToEdgeWrapping
+ texture.needsUpdate = true
+ }
// poor man's equi-portal
mesh.material = new THREE.ShaderMaterial( {
side: THREE.DoubleSide,
uniforms: {
- pano: { value: texture }
+ pano: { value: texture },
+ highlight: { value: false },
},
vertexShader: `
vec3 portalPosition;
@@ -925,6 +966,7 @@ xrf.frag.href = function(v, opts){
fragmentShader: `
#define RECIPROCAL_PI2 0.15915494
uniform sampler2D pano;
+ uniform bool highlight;
varying float vDistanceToCenter;
varying float vDistance;
varying vec3 vWorldPosition;
@@ -937,14 +979,14 @@ xrf.frag.href = function(v, opts){
vec4 color = texture2D(pano, sampleUV);
// Convert color to grayscale (lazy lite approach to not having to match tonemapping/shaderstacking of THREE.js)
float luminance = 0.2126 * color.r + 0.7152 * color.g + 0.0722 * color.b;
- vec4 grayscale_color = vec4(vec3(luminance) + vec3(0.33), color.a);
+ vec4 grayscale_color = highlight ? color : vec4(vec3(luminance) + vec3(0.33), color.a);
gl_FragColor = grayscale_color;
}
`,
});
mesh.material.needsUpdate = true
- mesh.handleTeleport = (e) => {
+ let teleport = mesh.userData.XRF.href.exec = (e) => {
if( mesh.clicked ) return
mesh.clicked = true
let portalArea = 1 // 1 meter
@@ -962,7 +1004,7 @@ xrf.frag.href = function(v, opts){
camera.position.copy(newPos);
camera.lookAt(meshWorldPosition);
- if( xrf.baseReferenceSpace ){ // WebXR VR/AR roomscale reposition
+ if( renderer.xr.isPresenting && xrf.baseReferenceSpace ){ // WebXR VR/AR roomscale reposition
const offsetPosition = { x: -newPos.x, y: 0, z: -newPos.z, w: 1 };
const offsetRotation = new THREE.Quaternion();
const transform = new XRRigidTransform( offsetPosition, offsetRotation );
@@ -970,22 +1012,25 @@ xrf.frag.href = function(v, opts){
xrf.renderer.xr.setReferenceSpace( teleportSpaceOffset );
}
- document.location.hash = `#pos=${camera.position.x},${camera.position.y},${camera.position.z}`;
}
const distance = camera.position.distanceTo(newPos);
- if( distance > portalArea ) positionInFrontOfPortal()
- else xrf.navigate.to(v.string) // ok let's surf to HREF!
+ if( renderer.xr.isPresenting && distance > portalArea ) positionInFrontOfPortal()
+ else xrf.navigator.to(v.string) // ok let's surf to HREF!
setTimeout( () => mesh.clicked = false, 200 ) // prevent double clicks
}
-
- if( !opts.frag.q ) mesh.addEventListener('click', mesh.handleTeleport )
- // lazy remove mesh (because we're inside a traverse)
- setTimeout( () => {
- model.interactive.add(mesh) // make clickable
- },200)
+ if( !opts.frag.q ){
+ mesh.addEventListener('click', teleport )
+ mesh.addEventListener('mousemove', () => mesh.material.uniforms.highlight.value = true )
+ mesh.addEventListener('nocollide', () => mesh.material.uniforms.highlight.value = false )
+ }
+
+ // lazy remove mesh (because we're inside a traverse)
+ setTimeout( (mesh) => {
+ xrf.interactive.add(mesh)
+ }, 300, mesh )
}
xrf.frag.pos = function(v, opts){
let { frag, mesh, model, camera, scene, renderer, THREE} = opts
@@ -1018,24 +1063,38 @@ xrf.frag.rot = function(v, opts){
xrf.frag.src = function(v, opts){
let { mesh, model, camera, scene, renderer, THREE} = opts
+
if( v.string[0] == "#" ){ // local
console.log(" └ instancing src")
let frag = xrfragment.URI.parse(v.string)
// Get an instance of the original model
- const modelInstance = new THREE.Group();
- let sceneInstance = model.scene.clone()
- modelInstance.add(sceneInstance)
- modelInstance.position.z = mesh.position.z
- modelInstance.position.y = mesh.position.y
- modelInstance.position.x = mesh.position.x
- modelInstance.scale.z = mesh.scale.x
- modelInstance.scale.y = mesh.scale.y
- modelInstance.scale.x = mesh.scale.z
- // now apply XR Fragments overrides from URI
- for( var i in frag )
- xrf.eval.fragment(i, Object.assign(opts,{frag, model:modelInstance,scene:sceneInstance}))
- // Add the instance to the scene
- model.scene.add(modelInstance);
+ let sceneInstance = new THREE.Group()
+ sceneInstance.isSrc = true
+
+ // prevent infinite recursion #1: skip src-instanced models
+ for ( let i in model.scene.children ) {
+ let child = model.scene.children[i]
+ if( child.isSrc ) continue;
+ sceneInstance.add( model.scene.children[i].clone() )
+ }
+
+ sceneInstance.position.copy( mesh.position )
+ sceneInstance.scale.copy(mesh.scale)
+ sceneInstance.updateMatrixWorld(true) // needed because we're going to move portals to the interactive-group
+
+ // apply embedded XR fragments
+ setTimeout( () => {
+ sceneInstance.traverse( (m) => {
+ if( m.userData && m.userData.src ) return ;//delete m.userData.src // prevent infinite recursion
+ xrf.eval.mesh(m,{scene,recursive:true})
+ })
+ // apply URI XR Fragments inside src-value
+ for( var i in frag ){
+ xrf.eval.fragment(i, Object.assign(opts,{frag, model:{scene:sceneInstance},scene:sceneInstance}))
+ }
+ // Add the instance to the scene
+ model.scene.add(sceneInstance);
+ },200)
}
}
window.AFRAME.registerComponent('xrf', {
@@ -1045,7 +1104,7 @@ window.AFRAME.registerComponent('xrf', {
init: function () {
if( !AFRAME.XRF ) this.initXRFragments()
if( typeof this.data == "string" ){
- AFRAME.XRF.navigate.to(this.data)
+ AFRAME.XRF.navigator.to(this.data)
.then( (model) => {
let gets = [ ...document.querySelectorAll('[xrf-get]') ]
gets.map( (g) => g.emit('update') )
@@ -1054,8 +1113,20 @@ window.AFRAME.registerComponent('xrf', {
},
initXRFragments: function(){
- let aScene = document.querySelector('a-scene')
+
+ // clear all current xrf-get entities when click back button or href
+ let clear = () => {
+ console.log("CLEARING!")
+ let els = [...document.querySelectorAll('[xrf-get]')]
+ console.dir(els)
+ els.map( (el) => el.parentNode.remove(el) )
+ console.log( document.querySelectorAll('[xrf-get]').length )
+ }
+ window.addEventListener('popstate', clear )
+ window.addEventListener('pushstate', clear )
+
// enable XR fragments
+ let aScene = document.querySelector('a-scene')
let XRF = AFRAME.XRF = xrfragment.init({
THREE,
camera: aScene.camera,
@@ -1076,25 +1147,21 @@ window.AFRAME.registerComponent('xrf', {
XRF.rot = camOverride
XRF.href = (xrf,v,opts) => { // convert portal to a-entity so AFRAME
- camOverride(xrf,v,opts) // raycaster can reach it
+ camOverride(xrf,v,opts) // raycaster can find & execute it
let {mesh,camera} = opts;
let el = document.createElement("a-entity")
el.setAttribute("xrf-get",mesh.name )
el.setAttribute("class","collidable")
- el.addEventListener("click", (e) => {
- mesh.handleTeleport() // *TODO* rename to fragment-neutral mesh.xrf.exec() e.g.
- $('#player').object3D.position.copy(camera.position)
- })
+ el.addEventListener("click", mesh.userData.XRF.href.exec )
$('a-scene').appendChild(el)
}
-
},
})
window.AFRAME.registerComponent('xrf-get', {
schema: {
name: {type: 'string'},
- duplicate: {type: 'boolean'}
+ clone: {type: 'boolean', default:false}
},
init: function () {
@@ -1110,14 +1177,12 @@ window.AFRAME.registerComponent('xrf-get', {
console.error("mesh with name '"+meshname+"' not found in model")
return;
}
- if( !this.data.duplicate ) mesh.parent.remove(mesh)
- if( this.mesh ) this.mesh.parent.remove(this.mesh) // cleanup old clone
- let clone = this.mesh = mesh.clone()
+ if( !this.data.clone ) mesh.parent.remove(mesh)
////mesh.updateMatrixWorld();
this.el.object3D.position.setFromMatrixPosition(scene.matrixWorld);
this.el.object3D.quaternion.setFromRotationMatrix(scene.matrixWorld);
- this.el.setObject3D('mesh', clone );
- if( !this.el.id ) this.el.setAttribute("id",`xrf-${clone.name}`)
+ this.el.setObject3D('mesh', mesh );
+ if( !this.el.id ) this.el.setAttribute("id",`xrf-${mesh.name}`)
})
diff --git a/dist/xrfragment.three.js b/dist/xrfragment.three.js
index 69d2509..380328a 100644
--- a/dist/xrfragment.three.js
+++ b/dist/xrfragment.three.js
@@ -620,6 +620,13 @@ xrfragment.InteractiveGroup = function(THREE,renderer,camera){
const raycaster = new Raycaster();
const tempMatrix = new Matrix4();
+ function nocollide(){
+ if( nocollide.tid ) return // ratelimit
+ _event.type = "nocollide"
+ scope.children.map( (c) => c.dispatchEvent(_event) )
+ nocollide.tid = setTimeout( () => nocollide.tid = null, 100 )
+ }
+
// Pointer Events
const element = renderer.domElement;
@@ -649,7 +656,7 @@ xrfragment.InteractiveGroup = function(THREE,renderer,camera){
object.dispatchEvent( _event );
- }
+ }else nocollide()
}
@@ -697,7 +704,7 @@ xrfragment.InteractiveGroup = function(THREE,renderer,camera){
object.dispatchEvent( _event );
- }
+ }else nocollide()
}
@@ -721,7 +728,7 @@ xrfragment.InteractiveGroup = function(THREE,renderer,camera){
}
let xrf = xrfragment
xrf.frag = {}
-xrf.model = {}
+xrf.model = {}
xrf.init = function(opts){
opts = opts || {}
@@ -732,8 +739,10 @@ xrf.init = function(opts){
for ( let i in xrf.XRF ) xrf.XRF[i] // shortcuts to constants (NAVIGATOR e.g.)
xrf.Parser.debug = xrf.debug
if( opts.loaders ) Object.values(opts.loaders).map( xrf.patchLoader )
+
+ xrf.interactive = xrf.InteractiveGroup( opts.THREE, opts.renderer, opts.camera)
+ xrf.scene.add( xrf.interactive)
xrf.patchRenderer(opts.renderer)
- xrf.navigate.init()
return xrf
}
@@ -763,15 +772,7 @@ xrf.parseModel = function(model,url){
let file = xrf.getFile(url)
model.file = file
model.render = function(){}
- model.interactive = xrf.InteractiveGroup( xrf.THREE, xrf.renderer, xrf.camera)
- model.scene.add(model.interactive)
-
- console.log("scanning "+file)
-
- model.scene.traverse( (mesh) => {
- console.log("◎ "+ (mesh.name||`THREE.${mesh.constructor.name}`))
- xrf.eval.mesh(mesh,model)
- })
+ model.scene.traverse( (mesh) => xrf.eval.mesh(mesh,model) )
}
xrf.getLastModel = () => xrf.model.last
@@ -800,6 +801,7 @@ xrf.eval.mesh = (mesh,model) => {
for( let k in mesh.userData ) xrf.Parser.parse( k, mesh.userData[k], frag )
for( let k in frag ){
let opts = {frag, mesh, model, camera: xrf.camera, scene: xrf.scene, renderer: xrf.renderer, THREE: xrf.THREE }
+ mesh.userData.XRF = frag // allow fragment impl to access XRF obj already
xrf.eval.fragment(k,opts)
}
}
@@ -813,52 +815,80 @@ xrf.eval.fragment = (k, opts ) => {
}
xrf.reset = () => {
- if( !xrf.model.scene ) return
- xrf.scene.remove( xrf.model.scene )
- xrf.model.scene.traverse( function(node){
- if( node instanceof xrf.THREE.Mesh ){
- node.geometry.dispose()
- node.material.dispose()
+ console.log("xrf.reset()")
+
+ const disposeObject = (obj) => {
+ if (obj.children.length > 0) obj.children.forEach((child) => disposeObject(child));
+ if (obj.geometry) obj.geometry.dispose();
+ if (obj.material) {
+ if (obj.material.map) obj.material.map.dispose();
+ obj.material.dispose();
}
+ return true
+ };
+
+ for ( let i in xrf.scene.children ) {
+ const child = xrf.scene.children[i];
+ if( child.xrf ){ // dont affect user objects
+ disposeObject(child);
+ xrf.scene.remove(child);
+ }
+ }
+ // remove interactive xrf objs like href-portals
+ xrf.interactive.traverse( (n) => {
+ if( disposeObject(n) ) xrf.interactive.remove(n)
})
}
-xrf.navigate = {}
+xrf.parseUrl = (url) => {
+ const urlObj = new URL( url.match(/:\/\//) ? url : String(`https://fake.com/${url}`).replace(/\/\//,'/') )
+ let dir = url.substring(0, url.lastIndexOf('/') + 1)
+ const file = urlObj.pathname.substring(urlObj.pathname.lastIndexOf('/') + 1);
+ const hash = url.match(/#/) ? url.replace(/.*#/,'') : ''
+ const ext = file.split('.').pop()
+ return {urlObj,dir,file,hash,ext}
+}
+xrf.navigator = {}
-xrf.navigate.to = (url) => {
+xrf.navigator.to = (url) => {
return new Promise( (resolve,reject) => {
console.log("xrfragment: navigating to "+url)
+ let {urlObj,dir,file,hash,ext} = xrf.parseUrl(url)
if( xrf.model && xrf.model.scene ) xrf.model.scene.visible = false
- const urlObj = new URL( url.match(/:\/\//) ? url : String(`https://fake.com/${url}`).replace(/\/\//,'/') )
- let dir = url.substring(0, url.lastIndexOf('/') + 1)
- const file = urlObj.pathname.substring(urlObj.pathname.lastIndexOf('/') + 1);
- const ext = file.split('.').pop()
+ console.log("ext="+ext)
const Loader = xrf.loaders[ext]
if( !Loader ) throw 'xrfragment: no loader passed to xrfragment for extension .'+ext
// force relative path
if( dir ) dir = dir[0] == '.' ? dir : `.${dir}`
const loader = new Loader().setPath( dir )
loader.load( file, function(model){
- xrf.scene.add( model.scene )
xrf.reset()
+ model.scene.xrf = true // leave mark for reset()
+ xrf.scene.add( model.scene )
xrf.model = model
- xrf.navigate.commit( file )
+ xrf.navigator.commit( file, hash )
resolve(model)
})
})
}
-xrf.navigate.init = () => {
- if( xrf.navigate.init.inited ) return
+xrf.navigator.init = () => {
+ if( xrf.navigator.init.inited ) return
window.addEventListener('popstate', function (event){
- console.dir(event)
- xrf.navigate.to( document.location.search.substr(1) + document.location.hash )
+ console.log(event.target.document.location.search)
+ console.log(event.currentTarget.document.location.search)
+ console.log(document.location.search)
+ xrf.navigator.to( document.location.search.substr(1) + document.location.hash )
})
- xrf.navigate.init.inited = true
+ let {url,urlObj,dir,file,hash,ext} = xrf.parseUrl(document.location.href)
+ //console.dir({file,hash})
+ xrf.navigator.commit(file,document.location.hash)
+ xrf.navigator.init.inited = true
}
-xrf.navigate.commit = (file) => {
- window.history.pushState({},null, document.location.pathname + `?${file}${document.location.hash}` )
+xrf.navigator.commit = (file,hash) => {
+ console.log("hash="+hash)
+ window.history.pushState({},null, document.location.pathname + `?${file}#${hash}` )
}
xrf.frag.env = function(v, opts){
let { mesh, model, camera, scene, renderer, THREE} = opts
@@ -898,16 +928,27 @@ xrf.frag.env = function(v, opts){
xrf.frag.href = function(v, opts){
let { mesh, model, camera, scene, renderer, THREE} = opts
+ const world = { pos: new THREE.Vector3(), scale: new THREE.Vector3() }
+ mesh.getWorldPosition(world.pos)
+ mesh.getWorldScale(world.scale)
+ mesh.position.copy(world.pos)
+ mesh.scale.copy(world.scale)
+ console.log("HREF: "+(model.recursive ?"src-instanced":"original"))
+
+ // convert texture if needed
let texture = mesh.material.map
- texture.mapping = THREE.ClampToEdgeWrapping
- texture.needsUpdate = true
- mesh.material.dispose()
+ if( texture && texture.source.data.height == texture.source.data.width/2 ){
+ // assume equirectangular image
+ texture.mapping = THREE.ClampToEdgeWrapping
+ texture.needsUpdate = true
+ }
// poor man's equi-portal
mesh.material = new THREE.ShaderMaterial( {
side: THREE.DoubleSide,
uniforms: {
- pano: { value: texture }
+ pano: { value: texture },
+ highlight: { value: false },
},
vertexShader: `
vec3 portalPosition;
@@ -925,6 +966,7 @@ xrf.frag.href = function(v, opts){
fragmentShader: `
#define RECIPROCAL_PI2 0.15915494
uniform sampler2D pano;
+ uniform bool highlight;
varying float vDistanceToCenter;
varying float vDistance;
varying vec3 vWorldPosition;
@@ -937,14 +979,14 @@ xrf.frag.href = function(v, opts){
vec4 color = texture2D(pano, sampleUV);
// Convert color to grayscale (lazy lite approach to not having to match tonemapping/shaderstacking of THREE.js)
float luminance = 0.2126 * color.r + 0.7152 * color.g + 0.0722 * color.b;
- vec4 grayscale_color = vec4(vec3(luminance) + vec3(0.33), color.a);
+ vec4 grayscale_color = highlight ? color : vec4(vec3(luminance) + vec3(0.33), color.a);
gl_FragColor = grayscale_color;
}
`,
});
mesh.material.needsUpdate = true
- mesh.handleTeleport = (e) => {
+ let teleport = mesh.userData.XRF.href.exec = (e) => {
if( mesh.clicked ) return
mesh.clicked = true
let portalArea = 1 // 1 meter
@@ -962,7 +1004,7 @@ xrf.frag.href = function(v, opts){
camera.position.copy(newPos);
camera.lookAt(meshWorldPosition);
- if( xrf.baseReferenceSpace ){ // WebXR VR/AR roomscale reposition
+ if( renderer.xr.isPresenting && xrf.baseReferenceSpace ){ // WebXR VR/AR roomscale reposition
const offsetPosition = { x: -newPos.x, y: 0, z: -newPos.z, w: 1 };
const offsetRotation = new THREE.Quaternion();
const transform = new XRRigidTransform( offsetPosition, offsetRotation );
@@ -970,22 +1012,25 @@ xrf.frag.href = function(v, opts){
xrf.renderer.xr.setReferenceSpace( teleportSpaceOffset );
}
- document.location.hash = `#pos=${camera.position.x},${camera.position.y},${camera.position.z}`;
}
const distance = camera.position.distanceTo(newPos);
- if( distance > portalArea ) positionInFrontOfPortal()
- else xrf.navigate.to(v.string) // ok let's surf to HREF!
+ if( renderer.xr.isPresenting && distance > portalArea ) positionInFrontOfPortal()
+ else xrf.navigator.to(v.string) // ok let's surf to HREF!
setTimeout( () => mesh.clicked = false, 200 ) // prevent double clicks
}
-
- if( !opts.frag.q ) mesh.addEventListener('click', mesh.handleTeleport )
- // lazy remove mesh (because we're inside a traverse)
- setTimeout( () => {
- model.interactive.add(mesh) // make clickable
- },200)
+ if( !opts.frag.q ){
+ mesh.addEventListener('click', teleport )
+ mesh.addEventListener('mousemove', () => mesh.material.uniforms.highlight.value = true )
+ mesh.addEventListener('nocollide', () => mesh.material.uniforms.highlight.value = false )
+ }
+
+ // lazy remove mesh (because we're inside a traverse)
+ setTimeout( (mesh) => {
+ xrf.interactive.add(mesh)
+ }, 300, mesh )
}
xrf.frag.pos = function(v, opts){
let { frag, mesh, model, camera, scene, renderer, THREE} = opts
@@ -1018,24 +1063,38 @@ xrf.frag.rot = function(v, opts){
xrf.frag.src = function(v, opts){
let { mesh, model, camera, scene, renderer, THREE} = opts
+
if( v.string[0] == "#" ){ // local
console.log(" └ instancing src")
let frag = xrfragment.URI.parse(v.string)
// Get an instance of the original model
- const modelInstance = new THREE.Group();
- let sceneInstance = model.scene.clone()
- modelInstance.add(sceneInstance)
- modelInstance.position.z = mesh.position.z
- modelInstance.position.y = mesh.position.y
- modelInstance.position.x = mesh.position.x
- modelInstance.scale.z = mesh.scale.x
- modelInstance.scale.y = mesh.scale.y
- modelInstance.scale.x = mesh.scale.z
- // now apply XR Fragments overrides from URI
- for( var i in frag )
- xrf.eval.fragment(i, Object.assign(opts,{frag, model:modelInstance,scene:sceneInstance}))
- // Add the instance to the scene
- model.scene.add(modelInstance);
+ let sceneInstance = new THREE.Group()
+ sceneInstance.isSrc = true
+
+ // prevent infinite recursion #1: skip src-instanced models
+ for ( let i in model.scene.children ) {
+ let child = model.scene.children[i]
+ if( child.isSrc ) continue;
+ sceneInstance.add( model.scene.children[i].clone() )
+ }
+
+ sceneInstance.position.copy( mesh.position )
+ sceneInstance.scale.copy(mesh.scale)
+ sceneInstance.updateMatrixWorld(true) // needed because we're going to move portals to the interactive-group
+
+ // apply embedded XR fragments
+ setTimeout( () => {
+ sceneInstance.traverse( (m) => {
+ if( m.userData && m.userData.src ) return ;//delete m.userData.src // prevent infinite recursion
+ xrf.eval.mesh(m,{scene,recursive:true})
+ })
+ // apply URI XR Fragments inside src-value
+ for( var i in frag ){
+ xrf.eval.fragment(i, Object.assign(opts,{frag, model:{scene:sceneInstance},scene:sceneInstance}))
+ }
+ // Add the instance to the scene
+ model.scene.add(sceneInstance);
+ },200)
}
}
export default xrfragment;
diff --git a/example/aframe/sandbox/index.html b/example/aframe/sandbox/index.html
index 620042a..74879ad 100644
--- a/example/aframe/sandbox/index.html
+++ b/example/aframe/sandbox/index.html
@@ -5,7 +5,6 @@
-
@@ -23,8 +22,8 @@
- sourcecode
- ⬇️ model
+
+
@@ -32,7 +31,7 @@
-
+
@@ -51,23 +50,13 @@
window.addEventListener('hashchange', () => {
window.AFRAME.XRF.eval( $('#uri').value = document.location.hash )
})
- if( document.location.hash.length < 2 ) document.location.hash = $('#uri').value
+ //if( document.location.hash.length < 2 ) document.location.hash = $('#uri').value
// add look-controls at last (otherwise it'll be buggy after scene-updates)
$('[camera]').setAttribute("look-controls","")
// add screenshot component with camera to capture proper equirects
$('a-scene').setAttribute("screenshot",{camera: "[camera]",width: 4096*2, height:2048*2})
- // turn certain query into AFRAME entities
- // AFRAME.XRF.href = (xrf,v,opts) => {
- // let {model,mesh} = opts
- // xrf(v,opts)
- // // convert to entity
- // let el = document.createElement("a-entity")
- // el.setAttribute("gltf-to-entity",{ name: mesh.name})
- // el.setAttribute("class","collidable")
- // $('a-scene').appendChild(el)
- // }
})
@@ -19,8 +18,8 @@
- sourcecode
- ⬇️ model
+
+
@@ -129,7 +128,7 @@
let file = document.location.search.length > 2 ? document.location.search.substr(1) : './../../assets/example3.gltf'
$('#model').setAttribute("href","./../../asset/"+file)
- XRF.navigate.to( file )
+ XRF.navigator.to( file )
// setup mouse controls
@@ -175,7 +174,8 @@
controllerGrip2.add( controllerModelFactory.createControllerModel( controllerGrip2 ) );
scene.add( controllerGrip2 );
- setupConsole( $('textarea') )
+
+ setupConsole()
// GUI
diff --git a/example/threejs/sandbox/main.css b/example/threejs/sandbox/main.css
deleted file mode 100644
index d496122..0000000
--- a/example/threejs/sandbox/main.css
+++ /dev/null
@@ -1,91 +0,0 @@
-body {
- margin: 0;
- background-color: #000;
- color: #fff;
- font-family: Monospace;
- font-size: 13px;
- line-height: 24px;
- overscroll-behavior: none;
-}
-
-a {
- color: #ff0;
- text-decoration: none;
-}
-
-a:hover {
- text-decoration: underline;
-}
-
-button {
- cursor: pointer;
- text-transform: uppercase;
-}
-
-#info {
- position: absolute;
- top: 0px;
- width: 100%;
- padding: 10px;
- box-sizing: border-box;
- text-align: center;
- -moz-user-select: none;
- -webkit-user-select: none;
- -ms-user-select: none;
- user-select: none;
- pointer-events: none;
- z-index: 1; /* TODO Solve this in HTML */
-}
-
-a, button, input, select {
- pointer-events: auto;
-}
-
-.lil-gui {
- z-index: 2 !important; /* TODO Solve this in HTML */
-}
-
-@media all and ( max-width: 640px ) {
- .lil-gui.root {
- right: auto;
- top: auto;
- max-height: 50%;
- max-width: 80%;
- bottom: 0;
- left: 0;
- }
-}
-
-#overlay {
- position: absolute;
- font-size: 16px;
- z-index: 2;
- top: 0;
- left: 0;
- width: 100%;
- height: 100%;
- display: flex;
- align-items: center;
- justify-content: center;
- flex-direction: column;
- background: rgba(0,0,0,0.7);
-}
-
- #overlay button {
- background: transparent;
- border: 0;
- border: 1px solid rgb(255, 255, 255);
- border-radius: 4px;
- color: #ffffff;
- padding: 12px 18px;
- text-transform: uppercase;
- cursor: pointer;
- }
-
-#notSupported {
- width: 50%;
- margin: auto;
- background-color: #f00;
- margin-top: 20px;
- padding: 10px;
-}
diff --git a/src/3rd/aframe/index.js b/src/3rd/aframe/index.js
index 93690d8..457a423 100644
--- a/src/3rd/aframe/index.js
+++ b/src/3rd/aframe/index.js
@@ -5,7 +5,7 @@ window.AFRAME.registerComponent('xrf', {
init: function () {
if( !AFRAME.XRF ) this.initXRFragments()
if( typeof this.data == "string" ){
- AFRAME.XRF.navigate.to(this.data)
+ AFRAME.XRF.navigator.to(this.data)
.then( (model) => {
let gets = [ ...document.querySelectorAll('[xrf-get]') ]
gets.map( (g) => g.emit('update') )
@@ -14,8 +14,20 @@ window.AFRAME.registerComponent('xrf', {
},
initXRFragments: function(){
- let aScene = document.querySelector('a-scene')
+
+ // clear all current xrf-get entities when click back button or href
+ let clear = () => {
+ console.log("CLEARING!")
+ let els = [...document.querySelectorAll('[xrf-get]')]
+ console.dir(els)
+ els.map( (el) => el.parentNode.remove(el) )
+ console.log( document.querySelectorAll('[xrf-get]').length )
+ }
+ window.addEventListener('popstate', clear )
+ window.addEventListener('pushstate', clear )
+
// enable XR fragments
+ let aScene = document.querySelector('a-scene')
let XRF = AFRAME.XRF = xrfragment.init({
THREE,
camera: aScene.camera,
@@ -36,25 +48,21 @@ window.AFRAME.registerComponent('xrf', {
XRF.rot = camOverride
XRF.href = (xrf,v,opts) => { // convert portal to a-entity so AFRAME
- camOverride(xrf,v,opts) // raycaster can reach it
+ camOverride(xrf,v,opts) // raycaster can find & execute it
let {mesh,camera} = opts;
let el = document.createElement("a-entity")
el.setAttribute("xrf-get",mesh.name )
el.setAttribute("class","collidable")
- el.addEventListener("click", (e) => {
- mesh.handleTeleport() // *TODO* rename to fragment-neutral mesh.xrf.exec() e.g.
- //$('#player').object3D.position.copy(camera.position)
- })
+ el.addEventListener("click", mesh.userData.XRF.href.exec )
$('a-scene').appendChild(el)
}
-
},
})
window.AFRAME.registerComponent('xrf-get', {
schema: {
name: {type: 'string'},
- duplicate: {type: 'boolean'}
+ clone: {type: 'boolean', default:false}
},
init: function () {
@@ -70,14 +78,12 @@ window.AFRAME.registerComponent('xrf-get', {
console.error("mesh with name '"+meshname+"' not found in model")
return;
}
- if( !this.data.duplicate ) mesh.parent.remove(mesh)
- if( this.mesh ) this.mesh.parent.remove(this.mesh) // cleanup old clone
- let clone = this.mesh = mesh.clone()
+ if( !this.data.clone ) mesh.parent.remove(mesh)
////mesh.updateMatrixWorld();
this.el.object3D.position.setFromMatrixPosition(scene.matrixWorld);
this.el.object3D.quaternion.setFromRotationMatrix(scene.matrixWorld);
- this.el.setObject3D('mesh', clone );
- if( !this.el.id ) this.el.setAttribute("id",`xrf-${clone.name}`)
+ this.el.setObject3D('mesh', mesh );
+ if( !this.el.id ) this.el.setAttribute("id",`xrf-${mesh.name}`)
})
diff --git a/src/3rd/three/InteractiveGroup.js b/src/3rd/three/InteractiveGroup.js
index 40fd1ad..f0ebf20 100644
--- a/src/3rd/three/InteractiveGroup.js
+++ b/src/3rd/three/InteractiveGroup.js
@@ -25,6 +25,13 @@ xrfragment.InteractiveGroup = function(THREE,renderer,camera){
const raycaster = new Raycaster();
const tempMatrix = new Matrix4();
+ function nocollide(){
+ if( nocollide.tid ) return // ratelimit
+ _event.type = "nocollide"
+ scope.children.map( (c) => c.dispatchEvent(_event) )
+ nocollide.tid = setTimeout( () => nocollide.tid = null, 100 )
+ }
+
// Pointer Events
const element = renderer.domElement;
@@ -54,7 +61,7 @@ xrfragment.InteractiveGroup = function(THREE,renderer,camera){
object.dispatchEvent( _event );
- }
+ }else nocollide()
}
@@ -102,7 +109,7 @@ xrfragment.InteractiveGroup = function(THREE,renderer,camera){
object.dispatchEvent( _event );
- }
+ }else nocollide()
}
diff --git a/src/3rd/three/index.js b/src/3rd/three/index.js
index f2c44b8..e1916d8 100644
--- a/src/3rd/three/index.js
+++ b/src/3rd/three/index.js
@@ -1,6 +1,6 @@
let xrf = xrfragment
xrf.frag = {}
-xrf.model = {}
+xrf.model = {}
xrf.init = function(opts){
opts = opts || {}
@@ -11,8 +11,10 @@ xrf.init = function(opts){
for ( let i in xrf.XRF ) xrf.XRF[i] // shortcuts to constants (NAVIGATOR e.g.)
xrf.Parser.debug = xrf.debug
if( opts.loaders ) Object.values(opts.loaders).map( xrf.patchLoader )
+
+ xrf.interactive = xrf.InteractiveGroup( opts.THREE, opts.renderer, opts.camera)
+ xrf.scene.add( xrf.interactive)
xrf.patchRenderer(opts.renderer)
- xrf.navigate.init()
return xrf
}
@@ -42,15 +44,7 @@ xrf.parseModel = function(model,url){
let file = xrf.getFile(url)
model.file = file
model.render = function(){}
- model.interactive = xrf.InteractiveGroup( xrf.THREE, xrf.renderer, xrf.camera)
- model.scene.add(model.interactive)
-
- console.log("scanning "+file)
-
- model.scene.traverse( (mesh) => {
- console.log("◎ "+ (mesh.name||`THREE.${mesh.constructor.name}`))
- xrf.eval.mesh(mesh,model)
- })
+ model.scene.traverse( (mesh) => xrf.eval.mesh(mesh,model) )
}
xrf.getLastModel = () => xrf.model.last
@@ -79,6 +73,7 @@ xrf.eval.mesh = (mesh,model) => {
for( let k in mesh.userData ) xrf.Parser.parse( k, mesh.userData[k], frag )
for( let k in frag ){
let opts = {frag, mesh, model, camera: xrf.camera, scene: xrf.scene, renderer: xrf.renderer, THREE: xrf.THREE }
+ mesh.userData.XRF = frag // allow fragment impl to access XRF obj already
xrf.eval.fragment(k,opts)
}
}
@@ -92,50 +87,36 @@ xrf.eval.fragment = (k, opts ) => {
}
xrf.reset = () => {
- if( !xrf.model.scene ) return
- xrf.scene.remove( xrf.model.scene )
- xrf.model.scene.traverse( function(node){
- if( node instanceof xrf.THREE.Mesh ){
- node.geometry.dispose()
- node.material.dispose()
+ console.log("xrf.reset()")
+
+ const disposeObject = (obj) => {
+ if (obj.children.length > 0) obj.children.forEach((child) => disposeObject(child));
+ if (obj.geometry) obj.geometry.dispose();
+ if (obj.material) {
+ if (obj.material.map) obj.material.map.dispose();
+ obj.material.dispose();
}
+ return true
+ };
+
+ for ( let i in xrf.scene.children ) {
+ const child = xrf.scene.children[i];
+ if( child.xrf ){ // dont affect user objects
+ disposeObject(child);
+ xrf.scene.remove(child);
+ }
+ }
+ // remove interactive xrf objs like href-portals
+ xrf.interactive.traverse( (n) => {
+ if( disposeObject(n) ) xrf.interactive.remove(n)
})
}
-xrf.navigate = {}
-
-xrf.navigate.to = (url) => {
- return new Promise( (resolve,reject) => {
- console.log("xrfragment: navigating to "+url)
- if( xrf.model && xrf.model.scene ) xrf.model.scene.visible = false
- const urlObj = new URL( url.match(/:\/\//) ? url : String(`https://fake.com/${url}`).replace(/\/\//,'/') )
- let dir = url.substring(0, url.lastIndexOf('/') + 1)
- const file = urlObj.pathname.substring(urlObj.pathname.lastIndexOf('/') + 1);
- const ext = file.split('.').pop()
- const Loader = xrf.loaders[ext]
- if( !Loader ) throw 'xrfragment: no loader passed to xrfragment for extension .'+ext
- // force relative path
- if( dir ) dir = dir[0] == '.' ? dir : `.${dir}`
- const loader = new Loader().setPath( dir )
- loader.load( file, function(model){
- xrf.scene.add( model.scene )
- xrf.reset()
- xrf.model = model
- xrf.navigate.commit( file )
- resolve(model)
- })
- })
-}
-
-xrf.navigate.init = () => {
- if( xrf.navigate.init.inited ) return
- window.addEventListener('popstate', function (event){
- console.dir(event)
- xrf.navigate.to( document.location.search.substr(1) + document.location.hash )
- })
- xrf.navigate.init.inited = true
-}
-
-xrf.navigate.commit = (file) => {
- window.history.pushState({},null, document.location.pathname + `?${file}${document.location.hash}` )
+xrf.parseUrl = (url) => {
+ const urlObj = new URL( url.match(/:\/\//) ? url : String(`https://fake.com/${url}`).replace(/\/\//,'/') )
+ let dir = url.substring(0, url.lastIndexOf('/') + 1)
+ const file = urlObj.pathname.substring(urlObj.pathname.lastIndexOf('/') + 1);
+ const hash = url.match(/#/) ? url.replace(/.*#/,'') : ''
+ const ext = file.split('.').pop()
+ return {urlObj,dir,file,hash,ext}
}
diff --git a/src/3rd/three/navigator.js b/src/3rd/three/navigator.js
new file mode 100644
index 0000000..719cb12
--- /dev/null
+++ b/src/3rd/three/navigator.js
@@ -0,0 +1,42 @@
+xrf.navigator = {}
+
+xrf.navigator.to = (url) => {
+ return new Promise( (resolve,reject) => {
+ console.log("xrfragment: navigating to "+url)
+ let {urlObj,dir,file,hash,ext} = xrf.parseUrl(url)
+ if( xrf.model && xrf.model.scene ) xrf.model.scene.visible = false
+ console.log("ext="+ext)
+ const Loader = xrf.loaders[ext]
+ if( !Loader ) throw 'xrfragment: no loader passed to xrfragment for extension .'+ext
+ // force relative path
+ if( dir ) dir = dir[0] == '.' ? dir : `.${dir}`
+ const loader = new Loader().setPath( dir )
+ loader.load( file, function(model){
+ xrf.reset()
+ model.scene.xrf = true // leave mark for reset()
+ xrf.scene.add( model.scene )
+ xrf.model = model
+ xrf.navigator.commit( file, hash )
+ resolve(model)
+ })
+ })
+}
+
+xrf.navigator.init = () => {
+ if( xrf.navigator.init.inited ) return
+ window.addEventListener('popstate', function (event){
+ console.log(event.target.document.location.search)
+ console.log(event.currentTarget.document.location.search)
+ console.log(document.location.search)
+ xrf.navigator.to( document.location.search.substr(1) + document.location.hash )
+ })
+ let {url,urlObj,dir,file,hash,ext} = xrf.parseUrl(document.location.href)
+ //console.dir({file,hash})
+ xrf.navigator.commit(file,document.location.hash)
+ xrf.navigator.init.inited = true
+}
+
+xrf.navigator.commit = (file,hash) => {
+ console.log("hash="+hash)
+ window.history.pushState({},null, document.location.pathname + `?${file}#${hash}` )
+}
diff --git a/src/3rd/three/xrf/href.js b/src/3rd/three/xrf/href.js
index f744d23..7558335 100644
--- a/src/3rd/three/xrf/href.js
+++ b/src/3rd/three/xrf/href.js
@@ -1,16 +1,27 @@
xrf.frag.href = function(v, opts){
let { mesh, model, camera, scene, renderer, THREE} = opts
+ const world = { pos: new THREE.Vector3(), scale: new THREE.Vector3() }
+ mesh.getWorldPosition(world.pos)
+ mesh.getWorldScale(world.scale)
+ mesh.position.copy(world.pos)
+ mesh.scale.copy(world.scale)
+ console.log("HREF: "+(model.recursive ?"src-instanced":"original"))
+
+ // convert texture if needed
let texture = mesh.material.map
- texture.mapping = THREE.ClampToEdgeWrapping
- texture.needsUpdate = true
- mesh.material.dispose()
+ if( texture && texture.source.data.height == texture.source.data.width/2 ){
+ // assume equirectangular image
+ texture.mapping = THREE.ClampToEdgeWrapping
+ texture.needsUpdate = true
+ }
// poor man's equi-portal
mesh.material = new THREE.ShaderMaterial( {
side: THREE.DoubleSide,
uniforms: {
- pano: { value: texture }
+ pano: { value: texture },
+ highlight: { value: false },
},
vertexShader: `
vec3 portalPosition;
@@ -28,6 +39,7 @@ xrf.frag.href = function(v, opts){
fragmentShader: `
#define RECIPROCAL_PI2 0.15915494
uniform sampler2D pano;
+ uniform bool highlight;
varying float vDistanceToCenter;
varying float vDistance;
varying vec3 vWorldPosition;
@@ -40,14 +52,14 @@ xrf.frag.href = function(v, opts){
vec4 color = texture2D(pano, sampleUV);
// Convert color to grayscale (lazy lite approach to not having to match tonemapping/shaderstacking of THREE.js)
float luminance = 0.2126 * color.r + 0.7152 * color.g + 0.0722 * color.b;
- vec4 grayscale_color = vec4(vec3(luminance) + vec3(0.33), color.a);
+ vec4 grayscale_color = highlight ? color : vec4(vec3(luminance) + vec3(0.33), color.a);
gl_FragColor = grayscale_color;
}
`,
});
mesh.material.needsUpdate = true
- mesh.handleTeleport = (e) => {
+ let teleport = mesh.userData.XRF.href.exec = (e) => {
if( mesh.clicked ) return
mesh.clicked = true
let portalArea = 1 // 1 meter
@@ -65,7 +77,7 @@ xrf.frag.href = function(v, opts){
camera.position.copy(newPos);
camera.lookAt(meshWorldPosition);
- if( xrf.baseReferenceSpace ){ // WebXR VR/AR roomscale reposition
+ if( renderer.xr.isPresenting && xrf.baseReferenceSpace ){ // WebXR VR/AR roomscale reposition
const offsetPosition = { x: -newPos.x, y: 0, z: -newPos.z, w: 1 };
const offsetRotation = new THREE.Quaternion();
const transform = new XRRigidTransform( offsetPosition, offsetRotation );
@@ -73,20 +85,23 @@ xrf.frag.href = function(v, opts){
xrf.renderer.xr.setReferenceSpace( teleportSpaceOffset );
}
- document.location.hash = `#pos=${camera.position.x},${camera.position.y},${camera.position.z}`;
}
const distance = camera.position.distanceTo(newPos);
- if( distance > portalArea ) positionInFrontOfPortal()
- else xrf.navigate.to(v.string) // ok let's surf to HREF!
+ if( renderer.xr.isPresenting && distance > portalArea ) positionInFrontOfPortal()
+ else xrf.navigator.to(v.string) // ok let's surf to HREF!
setTimeout( () => mesh.clicked = false, 200 ) // prevent double clicks
}
-
- if( !opts.frag.q ) mesh.addEventListener('click', mesh.handleTeleport )
- // lazy remove mesh (because we're inside a traverse)
- setTimeout( () => {
- model.interactive.add(mesh) // make clickable
- },200)
+ if( !opts.frag.q ){
+ mesh.addEventListener('click', teleport )
+ mesh.addEventListener('mousemove', () => mesh.material.uniforms.highlight.value = true )
+ mesh.addEventListener('nocollide', () => mesh.material.uniforms.highlight.value = false )
+ }
+
+ // lazy remove mesh (because we're inside a traverse)
+ setTimeout( (mesh) => {
+ xrf.interactive.add(mesh)
+ }, 300, mesh )
}
diff --git a/src/3rd/three/xrf/src.js b/src/3rd/three/xrf/src.js
index 66b573a..7e86b42 100644
--- a/src/3rd/three/xrf/src.js
+++ b/src/3rd/three/xrf/src.js
@@ -2,23 +2,37 @@
xrf.frag.src = function(v, opts){
let { mesh, model, camera, scene, renderer, THREE} = opts
+
if( v.string[0] == "#" ){ // local
console.log(" └ instancing src")
let frag = xrfragment.URI.parse(v.string)
// Get an instance of the original model
- const modelInstance = new THREE.Group();
- let sceneInstance = model.scene.clone()
- modelInstance.add(sceneInstance)
- modelInstance.position.z = mesh.position.z
- modelInstance.position.y = mesh.position.y
- modelInstance.position.x = mesh.position.x
- modelInstance.scale.z = mesh.scale.x
- modelInstance.scale.y = mesh.scale.y
- modelInstance.scale.x = mesh.scale.z
- // now apply XR Fragments overrides from URI
- for( var i in frag )
- xrf.eval.fragment(i, Object.assign(opts,{frag, model:modelInstance,scene:sceneInstance}))
- // Add the instance to the scene
- model.scene.add(modelInstance);
+ let sceneInstance = new THREE.Group()
+ sceneInstance.isSrc = true
+
+ // prevent infinite recursion #1: skip src-instanced models
+ for ( let i in model.scene.children ) {
+ let child = model.scene.children[i]
+ if( child.isSrc ) continue;
+ sceneInstance.add( model.scene.children[i].clone() )
+ }
+
+ sceneInstance.position.copy( mesh.position )
+ sceneInstance.scale.copy(mesh.scale)
+ sceneInstance.updateMatrixWorld(true) // needed because we're going to move portals to the interactive-group
+
+ // apply embedded XR fragments
+ setTimeout( () => {
+ sceneInstance.traverse( (m) => {
+ if( m.userData && m.userData.src ) return ;//delete m.userData.src // prevent infinite recursion
+ xrf.eval.mesh(m,{scene,recursive:true})
+ })
+ // apply URI XR Fragments inside src-value
+ for( var i in frag ){
+ xrf.eval.fragment(i, Object.assign(opts,{frag, model:{scene:sceneInstance},scene:sceneInstance}))
+ }
+ // Add the instance to the scene
+ model.scene.add(sceneInstance);
+ },200)
}
}
diff --git a/src/3rd/wasm/README.md b/src/3rd/wasm/README.md
new file mode 100644
index 0000000..8d04a79
--- /dev/null
+++ b/src/3rd/wasm/README.md
@@ -0,0 +1,2 @@
+javy compile index.js -o index.wasm
+echo '{ "n": 2, "bar": "baz" }' | wasmtime index.wasm
diff --git a/src/3rd/wasm/index.js b/src/3rd/wasm/index.js
new file mode 100644
index 0000000..843764c
--- /dev/null
+++ b/src/3rd/wasm/index.js
@@ -0,0 +1,50 @@
+// Read input from stdin
+const input = readInput();
+// Call the function with the input
+const result = foo(input);
+// Write the result to stdout
+writeOutput(result);
+
+// The main function.
+function foo(input) {
+ return { foo: input.n + 1, newBar: input.bar + "!" };
+}
+
+// Read input from stdin
+function readInput() {
+ const chunkSize = 1024;
+ const inputChunks = [];
+ let totalBytes = 0;
+
+ // Read all the available bytes
+ while (1) {
+ const buffer = new Uint8Array(chunkSize);
+ // Stdin file descriptor
+ const fd = 0;
+ const bytesRead = Javy.IO.readSync(fd, buffer);
+
+ totalBytes += bytesRead;
+ if (bytesRead === 0) {
+ break;
+ }
+ inputChunks.push(buffer.subarray(0, bytesRead));
+ }
+
+ // Assemble input into a single Uint8Array
+ const { finalBuffer } = inputChunks.reduce((context, chunk) => {
+ context.finalBuffer.set(chunk, context.bufferOffset);
+ context.bufferOffset += chunk.length;
+ return context;
+ }, { bufferOffset: 0, finalBuffer: new Uint8Array(totalBytes) });
+
+ return JSON.parse(new TextDecoder().decode(finalBuffer));
+}
+
+// Write output to stdout
+function writeOutput(output) {
+ const encodedOutput = new TextEncoder().encode(JSON.stringify(output));
+ const buffer = new Uint8Array(encodedOutput);
+ // Stdout file descriptor
+ const fd = 1;
+ Javy.IO.writeSync(fd, buffer);
+}
diff --git a/src/3rd/wasm/index.wasm b/src/3rd/wasm/index.wasm
new file mode 100644
index 0000000000000000000000000000000000000000..cc254d30c3bfa9cea0d1674237d1876663db3169
GIT binary patch
literal 895562
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