work in progress [might break]

This commit is contained in:
Leon van Kammen 2023-05-17 21:31:28 +02:00
parent c626f7c762
commit 81e9aca075
17 changed files with 612 additions and 419 deletions

View file

@ -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}`)
})

View file

@ -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;

View file

@ -5,7 +5,6 @@
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<link rel="stylesheet" href="./../../assets/axist.min.css" />
<link type="text/css" rel="stylesheet" href="./main.css">
<link type="text/css" rel="stylesheet" href="./../../assets/style.css"/>
<script async src="./../../assets/alpine.min.js" defer></script>
<script src="https://aframe.io/releases/1.4.2/aframe.min.js"></script>
@ -23,8 +22,8 @@
</template>
</datalist>
</div>
<a id="source" target="_blank" href="https://github.com/coderofsalvation/xrfragment/blob/main/example/aframe/sandbox/index.html">sourcecode</a>
<a id="model" target="_blank" href="">⬇️ model</a>
<a class="btn-foot" id="source" target="_blank" href="https://github.com/coderofsalvation/xrfragment/blob/main/example/aframe/sandbox/index.html">sourcecode</a>
<a class="btn-foot" id="model" target="_blank" href="">⬇️ model</a>
<textarea style="display:none"></textarea>
<a-scene light="defaultLightsEnabled: false">
<a-entity xrf id="player" >
@ -32,7 +31,7 @@
<a-entity id="left-hand" laser-controls="hand: left" raycaster="objects:.collidable;far:5500" oculus-touch-controls="hand: left" blink-controls="cameraRig:#player; teleportOrigin: #camera; collisionEntities: #floor"></a-entity>
<a-entity id="right-hand" laser-controls="hand: right" raycaster="objects:.collidable;far:5500" oculus-touch-controls="hand: right" blink-controls="cameraRig:#player; teleportOrigin: #camera; collisionEntities: #floor"></a-entity>
</a-entity>
<a-entity id="home" xrf="example3.gltf"></a-entity>
<a-entity id="home" xrf="example3.gltf#pos=1,2,3"></a-entity>
<a-entity id="floor" xrf-get="floor"></a-entity>
</a-scene>
@ -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)
// }
})
</script>
</body>

File diff suppressed because one or more lines are too long

View file

@ -72,25 +72,29 @@ input[type="submit"] {
border-color: #Ccc;
}
a#source{
.btn-foot{
background: white;
border-radius: 10px;
border: 5px solid #1c1c3299;
padding: 0px 6px;
bottom:19px;
position: absolute;
bottom: 29px;
right: 20px;
}
a.btn-foot#source{
right: 10px;
color: #888;
font-weight: bold;
font-family: sans-serif;
text-decoration: underline;
z-index:2000;
}
a#model{
a.btn-foot#model{
position: absolute;
bottom: 29px;
right: 130px;
color: #888;
font-weight: bold;
font-family: sans-serif;
text-decoration: underline;
z-index:2000;
}
@ -109,8 +113,8 @@ html.a-fullscreen a#source{
.lil-gui.autoPlace{
right:0px !important;
top:auto !important;
bottom:0;
top:48px !important;
height:33vh;
}
#VRButton {

View file

@ -20,21 +20,27 @@ export function loadFile(contentLoaders, multiple){
}
}
export function setupConsole($console){
export function setupConsole(el){
if( !el ) return setTimeout( () => setupConsole( $('.lil-gui') ),200 )
let $console = document.createElement('textarea')
$console.style.position = 'absolute'
$console.style.display = 'block'
$console.style.zIndex = 1000;
$console.style.zIndex = 2000;
$console.style.background = "transparent !important"
$console.style.pointerEvents = 'none'
$console.style.top = '70px'
$console.style.padding = '5px 20px 25px 25px'
$console.style.padding = '10px'
$console.style.margin = '10px'
$console.style.background = '#000'
$console.style.left = $console.style.right = $console.style.bottom = 0;
$console.style.color = '#0008';
$console.style.fontSize = '12px';
$console.style.color = '#A6F';
$console.style.fontSize = '10px';
$console.style.fontFamily = 'Courier'
$console.style.border = '0'
$console.innerHTML = "XRFRAGMENT CONSOLE OUTPUT:\n"
el.appendChild($console)
console.log = ( (log) => function(){
let s = new Date().toISOString().substr(11).substr(0,8) + " " + ([...arguments]).join(" ").replace(/.*[0-9]: /,"")
log(s)

View file

@ -5,7 +5,6 @@
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<link rel="stylesheet" href="./../../assets/axist.min.css" />
<link type="text/css" rel="stylesheet" href="./main.css">
<link type="text/css" rel="stylesheet" href="./../../assets/style.css"/>
</head>
<body>
@ -19,8 +18,8 @@
</template>
</datalist>
</div>
<a id="source" target="_blank" href="https://github.com/coderofsalvation/xrfragment/blob/main/example/threejs/sandbox/index.html#L92-L112">sourcecode</a>
<a id="model" target="_blank" href="">⬇️ model</a>
<a class="btn-foot" id="source" target="_blank" href="https://github.com/coderofsalvation/xrfragment/blob/main/example/threejs/sandbox/index.html#L92-L112">sourcecode</a>
<a class="btn-foot" id="model" target="_blank" href="">⬇️ model</a>
<textarea style="display:none"></textarea>
<script async src="./../../assets/alpine.min.js"></script>
@ -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

View file

@ -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;
}

View file

@ -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}`)
})

View file

@ -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()
}

View file

@ -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}
}

View file

@ -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}` )
}

View file

@ -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 )
}

View file

@ -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)
}
}

2
src/3rd/wasm/README.md Normal file
View file

@ -0,0 +1,2 @@
javy compile index.js -o index.wasm
echo '{ "n": 2, "bar": "baz" }' | wasmtime index.wasm

50
src/3rd/wasm/index.js Normal file
View file

@ -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);
}

BIN
src/3rd/wasm/index.wasm Normal file

Binary file not shown.