work in progress [might break]
This commit is contained in:
parent
c626f7c762
commit
81e9aca075
17 changed files with 612 additions and 419 deletions
221
dist/xrfragment.aframe.js
vendored
221
dist/xrfragment.aframe.js
vendored
|
|
@ -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}`)
|
||||
|
||||
})
|
||||
|
||||
|
|
|
|||
187
dist/xrfragment.three.js
vendored
187
dist/xrfragment.three.js
vendored
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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}`)
|
||||
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
}
|
||||
|
|
|
|||
42
src/3rd/three/navigator.js
Normal file
42
src/3rd/three/navigator.js
Normal 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}` )
|
||||
}
|
||||
|
|
@ -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 )
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
2
src/3rd/wasm/README.md
Normal 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
50
src/3rd/wasm/index.js
Normal 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
BIN
src/3rd/wasm/index.wasm
Normal file
Binary file not shown.
Loading…
Add table
Reference in a new issue