href work in progress + aframe simplify refactor

This commit is contained in:
Leon van Kammen 2023-05-12 22:06:21 +02:00
parent 21ffc80e07
commit 3cd5a7d333
21 changed files with 2283 additions and 2340 deletions

2
.vimrc
View File

@ -1,4 +1,4 @@
noremap <silent> <F8> :!haxe --no-output %<CR> noremap <silent> <F8> :!haxe --no-output %<CR>
noremap <silent> <F9> :!./make doc<CR> noremap <silent> <F9> :!./make build_js<CR>
noremap <silent> <F10> :!./make && echo OK && ./make tests<CR> noremap <silent> <F10> :!./make && echo OK && ./make tests<CR>
noremap <silent> <F11> :!./make tests \| less<CR> noremap <silent> <F11> :!./make tests \| less<CR>

View File

@ -593,114 +593,317 @@ xrfragment_XRF.isUrlOrPretypedView = new EReg("(^#|://)?\\..*","");
xrfragment_XRF.isString = new EReg(".*",""); xrfragment_XRF.isString = new EReg(".*","");
})({}); })({});
var xrfragment = $hx_exports["xrfragment"]; var xrfragment = $hx_exports["xrfragment"];
xrfragment.xrf = {} // wrapper to survive in/outside modules
xrfragment.model = {}
xrfragment.init = function(opts){ xrfragment.InteractiveGroup = function(THREE,renderer,camera){
let {
Group,
Matrix4,
Raycaster,
Vector2
} = THREE
const _pointer = new Vector2();
const _event = { type: '', data: _pointer };
class InteractiveGroup extends Group {
constructor( renderer, camera ) {
super();
if( !renderer || !camera ) return
const scope = this;
const raycaster = new Raycaster();
const tempMatrix = new Matrix4();
// Pointer Events
const element = renderer.domElement;
function onPointerEvent( event ) {
//event.stopPropagation();
const rect = renderer.domElement.getBoundingClientRect();
_pointer.x = ( event.clientX - rect.left ) / rect.width * 2 - 1;
_pointer.y = - ( event.clientY - rect.top ) / rect.height * 2 + 1;
raycaster.setFromCamera( _pointer, camera );
const intersects = raycaster.intersectObjects( scope.children, false );
if ( intersects.length > 0 ) {
const intersection = intersects[ 0 ];
const object = intersection.object;
const uv = intersection.uv;
_event.type = event.type;
_event.data.set( uv.x, 1 - uv.y );
object.dispatchEvent( _event );
}
}
element.addEventListener( 'pointerdown', onPointerEvent );
element.addEventListener( 'pointerup', onPointerEvent );
element.addEventListener( 'pointermove', onPointerEvent );
element.addEventListener( 'mousedown', onPointerEvent );
element.addEventListener( 'mouseup', onPointerEvent );
element.addEventListener( 'mousemove', onPointerEvent );
element.addEventListener( 'click', onPointerEvent );
// WebXR Controller Events
// TODO: Dispatch pointerevents too
const events = {
'move': 'mousemove',
'select': 'click',
'selectstart': 'mousedown',
'selectend': 'mouseup'
};
function onXRControllerEvent( event ) {
const controller = event.target;
tempMatrix.identity().extractRotation( controller.matrixWorld );
raycaster.ray.origin.setFromMatrixPosition( controller.matrixWorld );
raycaster.ray.direction.set( 0, 0, - 1 ).applyMatrix4( tempMatrix );
const intersections = raycaster.intersectObjects( scope.children, false );
if ( intersections.length > 0 ) {
const intersection = intersections[ 0 ];
const object = intersection.object;
const uv = intersection.uv;
_event.type = events[ event.type ];
_event.data.set( uv.x, 1 - uv.y );
if( _event.type != "mousemove" ){
console.log(event.type+" => "+_event.type)
}
object.dispatchEvent( _event );
}
}
const controller1 = renderer.xr.getController( 0 );
controller1.addEventListener( 'move', onXRControllerEvent );
controller1.addEventListener( 'select', onXRControllerEvent );
controller1.addEventListener( 'selectstart', onXRControllerEvent );
controller1.addEventListener( 'selectend', onXRControllerEvent );
const controller2 = renderer.xr.getController( 1 );
controller2.addEventListener( 'move', onXRControllerEvent );
controller2.addEventListener( 'select', onXRControllerEvent );
controller2.addEventListener( 'selectstart', onXRControllerEvent );
controller2.addEventListener( 'selectend', onXRControllerEvent );
}
}
return new InteractiveGroup(renderer,camera)
}
let xrf = xrfragment
xrf.frag = {}
xrf.model = {}
xrf.init = function(opts){
opts = opts || {} opts = opts || {}
let XRF = function(){ let XRF = function(){
alert("queries are not implemented (yet)") alert("queries are not implemented (yet)")
} }
for ( let i in opts ) xrfragment[i] = opts[i] for ( let i in opts ) xrf[i] = opts[i]
for ( let i in xrfragment.XRF ) xrfragment.XRF[i] // shortcuts to constants (NAVIGATOR e.g.) for ( let i in xrf.XRF ) xrf.XRF[i] // shortcuts to constants (NAVIGATOR e.g.)
xrfragment.Parser.debug = xrfragment.debug xrf.Parser.debug = xrf.debug
if( opts.loaders ) opts.loaders.map( xrfragment.patchLoader ) if( opts.loaders ) Object.values(opts.loaders).map( xrf.patchLoader )
xrfragment.patchRenderer(opts.renderer) xrf.patchRenderer(opts.renderer)
return xrfragment xrf.navigate.init()
return xrf
} }
xrfragment.patchRenderer = function(renderer){ xrf.patchRenderer = function(renderer){
renderer.xr.addEventListener( 'sessionstart', () => xrf.baseReferenceSpace = renderer.xr.getReferenceSpace() );
renderer.xr.enabled = true;
renderer.render = ((render) => function(scene,camera){ renderer.render = ((render) => function(scene,camera){
if( xrfragment.getLastModel() && xrfragment.getLastModel().render ) if( xrf.model && xrf.model.render )
xrfragment.getLastModel().render(scene,camera) xrf.model.render(scene,camera)
render(scene,camera) render(scene,camera)
})(renderer.render.bind(renderer)) })(renderer.render.bind(renderer))
} }
xrfragment.patchLoader = function(loader){ xrf.patchLoader = function(loader){
loader.prototype.load = ((load) => function(url, onLoad, onProgress, onError){ loader.prototype.load = ((load) => function(url, onLoad, onProgress, onError){
load.call( this, load.call( this,
url, url,
(model) => { onLoad(model); xrfragment.parseModel(model,url) }, (model) => { onLoad(model); xrf.parseModel(model,url) },
onProgress, onProgress,
onError) onError)
})(loader.prototype.load) })(loader.prototype.load)
} }
xrfragment.getFile = (url) => url.split("/").pop().replace(/#.*/,'') xrf.getFile = (url) => url.split("/").pop().replace(/#.*/,'')
xrfragment.parseModel = function(model,url){ xrf.parseModel = function(model,url){
let file = xrfragment.getFile(url) let file = xrf.getFile(url)
model.file = file model.file = file
model.render = function(){} model.render = function(){}
xrfragment.model[file] = model model.interactive = xrf.InteractiveGroup( xrf.THREE, xrf.renderer, xrf.camera)
model.scene.add(model.interactive)
console.log("scanning "+file) console.log("scanning "+file)
model.scene.traverse( (mesh) => { model.scene.traverse( (mesh) => {
console.log("◎ "+mesh.name) console.log("◎ "+ (mesh.name||`THREE.${mesh.constructor.name}`))
if( mesh.userData ){ xrf.eval.mesh(mesh,model)
let frag = {}
for( let k in mesh.userData ) xrfragment.Parser.parse( k, mesh.userData[k], frag )
for( let k in frag ){
let opts = {frag, mesh, model, camera: xrfragment.camera, scene: xrfragment.scene, renderer: xrfragment.renderer, THREE: xrfragment.THREE }
xrfragment.evalFragment(k,opts)
}
}
}) })
} }
xrfragment.evalFragment = (k, opts ) => { xrf.getLastModel = () => xrf.model.last
// call native function (xrf/env.js e.g.), or pass it to user decorator
let func = xrfragment.xrf[k] || function(){}
if( xrfragment[k] ) xrfragment[k]( func, opts.frag[k], opts)
else func( opts.frag[k], opts)
}
xrfragment.getLastModel = () => Object.values(xrfragment.model)[ Object.values(xrfragment.model).length-1 ] xrf.eval = function( url, model ){
xrfragment.eval = function( url, model ){
let notice = false let notice = false
model = model || xrfragment.getLastModel() model = model || xrf.model
let { THREE, camera } = xrfragment let { THREE, camera } = xrf
let frag = xrfragment.URI.parse( url, xrfragment.XRF.NAVIGATOR ) let frag = xrf.URI.parse( url, xrf.XRF.NAVIGATOR )
let meshes = frag.q ? [] : [camera] let meshes = frag.q ? [] : [camera]
for ( let i in meshes ) { for ( let i in meshes ) {
for ( let k in frag ){ for ( let k in frag ){
let mesh = meshes[i] let mesh = meshes[i]
if( !String(k).match(/(pos|rot)/) ) notice = true if( !String(k).match(/(pos|rot)/) ) notice = true
let opts = {frag, mesh, model, camera: xrfragment.camera, scene: xrfragment.scene, renderer: xrfragment.renderer, THREE: xrfragment.THREE } let opts = {frag, mesh, model, camera: xrf.camera, scene: xrf.scene, renderer: xrf.renderer, THREE: xrf.THREE }
xrfragment.evalFragment(k,opts) xrf.eval.fragment(k,opts)
} }
} }
if( notice ) alert("only 'pos' and 'rot' XRF.NAVIGATOR-flagged XR fragments are supported (for now)") if( notice ) alert("only 'pos' and 'rot' XRF.NAVIGATOR-flagged XR fragments are supported (for now)")
} }
xrfragment.xrf.env = function(v, opts){
xrf.eval.mesh = (mesh,model) => {
if( mesh.userData ){
let frag = {}
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 }
xrf.eval.fragment(k,opts)
}
}
}
xrf.eval.fragment = (k, opts ) => {
// call native function (xrf/env.js e.g.), or pass it to user decorator
let func = xrf.frag[k] || function(){}
if( xrf[k] ) xrf[k]( func, opts.frag[k], opts)
else func( opts.frag[k], opts)
}
xrf.reset = () => {
if( !xrf.model.scene ) return
xrf.scene.remove( xrf.model.scene )
xrf.model.scene.traverse( function(node){
if( node instanceof THREE.Mesh ){
node.geometry.dispose()
node.material.dispose()
}
})
}
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.frag.env = function(v, opts){
let { mesh, model, camera, scene, renderer, THREE} = opts let { mesh, model, camera, scene, renderer, THREE} = opts
let env = mesh.getObjectByName(v.string) let env = mesh.getObjectByName(v.string)
env.material.map.mapping = THREE.EquirectangularReflectionMapping; env.material.map.mapping = THREE.EquirectangularReflectionMapping;
scene.environment = env.material.map scene.environment = env.material.map
scene.texture = env.material.map //scene.texture = env.material.map
renderer.toneMapping = THREE.ACESFilmicToneMapping; renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure = 1; renderer.toneMappingExposure = 2;
// apply to meshes *DISABLED* renderer.environment does this
const maxAnisotropy = renderer.capabilities.getMaxAnisotropy();
setTimeout( () => {
scene.traverse( (mesh) => {
//if (mesh.material && mesh.material.map && mesh.material.metalness == 1.0) {
// mesh.material = new THREE.MeshBasicMaterial({ map: mesh.material.map });
// mesh.material.dithering = true
// mesh.material.map.anisotropy = maxAnisotropy;
// mesh.material.needsUpdate = true;
//}
//if (mesh.material && mesh.material.metalness == 1.0 ){
// mesh.material = new THREE.MeshBasicMaterial({
// color:0xffffff,
// emissive: mesh.material.map,
// envMap: env.material.map,
// side: THREE.DoubleSide,
// flatShading: true
// })
// mesh.material.needsUpdate = true
// //mesh.material.envMap = env.material.map;
// //mesh.material.envMap.intensity = 5;
// //mesh.material.needsUpdate = true;
//}
});
},500)
console.log(` └ applied image '${v.string}' as environment map`) console.log(` └ applied image '${v.string}' as environment map`)
} }
xrfragment.xrf.href = function(v, opts){ xrf.frag.href = function(v, opts){
let { mesh, model, camera, scene, renderer, THREE} = opts let { mesh, model, camera, scene, renderer, THREE} = opts
let size = 5
let texture = mesh.material.map let texture = mesh.material.map
texture.mapping = THREE.ClampToEdgeWrapping
texture.needsUpdate = true
mesh.material.dispose()
/* // poor man's equi-portal
texture.wrapS = texture.wrapT = THREE.ClampToEdgeWrapping;
mesh.material = new THREE.MeshStandardMaterial( {
envMap: texture,
roughness: 0.0,
metalness: 1,
side: THREE.DoubleSide,
})
*/
mesh.material = new THREE.ShaderMaterial( { mesh.material = new THREE.ShaderMaterial( {
side: THREE.DoubleSide, side: THREE.DoubleSide,
uniforms: { uniforms: {
@ -729,33 +932,99 @@ xrfragment.xrf.href = function(v, opts){
vec3 direction = normalize(vWorldPosition - cameraPosition); vec3 direction = normalize(vWorldPosition - cameraPosition);
vec2 sampleUV; vec2 sampleUV;
sampleUV.y = -clamp(direction.y * 0.5 + 0.5, 0.0, 1.0); sampleUV.y = -clamp(direction.y * 0.5 + 0.5, 0.0, 1.0);
sampleUV.x = atan(direction.z, -direction.x) * -RECIPROCAL_PI2 + 0.5; sampleUV.x = atan(direction.z, -direction.x) * -RECIPROCAL_PI2;
gl_FragColor = texture2D(pano, sampleUV); sampleUV.x += 0.33; // adjust focus to AFRAME's $('a-scene').components.screenshot.capture()
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);
gl_FragColor = grayscale_color;
} }
` `,
}); });
mesh.material.needsUpdate = true
const handleTeleport = (e) => {
if( mesh.clicked ) return
this.clicked = true
let portalArea = 1 // 1 meter
const meshWorldPosition = new THREE.Vector3();
meshWorldPosition.setFromMatrixPosition(mesh.matrixWorld);
const cameraDirection = new THREE.Vector3();
camera.getWorldPosition(cameraDirection);
cameraDirection.sub(meshWorldPosition);
cameraDirection.normalize();
cameraDirection.multiplyScalar(portalArea); // move away from portal
const newPos = meshWorldPosition.clone().add(cameraDirection);
const positionInFrontOfPortal = () => {
camera.position.copy(newPos);
camera.lookAt(meshWorldPosition);
if( 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 );
const teleportSpaceOffset = xrf.baseReferenceSpace.getOffsetReferenceSpace( transform );
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!
setTimeout( () => mesh.clicked = false, 200 ) // prevent double clicks
}
if( !opts.frag.q ) mesh.addEventListener('click', handleTeleport )
// lazy remove mesh (because we're inside a traverse)
setTimeout( () => {
model.interactive.add(mesh) // make clickable
},200)
} }
xrfragment.xrf.pos = function(v, opts){ xrf.frag.pos = function(v, opts){
let { frag, mesh, model, camera, scene, renderer, THREE} = opts let { frag, mesh, model, camera, scene, renderer, THREE} = opts
console.log(" └ setting camera position to "+v.string) console.log(" └ setting camera position to "+v.string)
camera.position.x = v.x camera.position.x = v.x
camera.position.y = v.y camera.position.y = v.y
camera.position.z = v.z camera.position.z = v.z
} }
xrfragment.xrf.rot = function(v, opts){ xrf.frag.q = function(v, opts){
let { frag, mesh, model, camera, scene, renderer, THREE} = opts
console.log(" └ running query ")
for ( let i in v.query ) {
let target = v.query[i]
// remove objects if requested
if( target.id != undefined && (target.mesh = scene.getObjectByName(i)) ){
target.mesh.visible = target.id
target.mesh.parent.remove(target.mesh)
console.log(` └ removing mesh: ${i}`)
}else console.log(` └ mesh not found: ${i}`)
}
}
xrf.frag.rot = function(v, opts){
let { mesh, model, camera, scene, renderer, THREE} = opts let { mesh, model, camera, scene, renderer, THREE} = opts
camera.rotation.x = v.x * Math.PI / 180; camera.rotation.x = v.x * Math.PI / 180;
camera.rotation.y = v.y * Math.PI / 180; camera.rotation.y = v.y * Math.PI / 180;
camera.rotation.z = v.z * Math.PI / 180; camera.rotation.z = v.z * Math.PI / 180;
} }
xrfragment.xrf.src = function(v, opts){ // *TODO* use webgl instancing
xrf.frag.src = function(v, opts){
let { mesh, model, camera, scene, renderer, THREE} = opts let { mesh, model, camera, scene, renderer, THREE} = opts
if( v.string[0] == "#" ){ // local if( v.string[0] == "#" ){ // local
console.log(" └ instancing src") console.log(" └ instancing src")
let args = xrfragment.URI.parse(v.string) let frag = xrfragment.URI.parse(v.string)
// Get an instance of the original model // Get an instance of the original model
const modelInstance = new THREE.Group(); const modelInstance = new THREE.Group();
modelInstance.add(model.scene.clone()); let sceneInstance = model.scene.clone()
modelInstance.add(sceneInstance)
modelInstance.position.z = mesh.position.z modelInstance.position.z = mesh.position.z
modelInstance.position.y = mesh.position.y modelInstance.position.y = mesh.position.y
modelInstance.position.x = mesh.position.x modelInstance.position.x = mesh.position.x
@ -763,19 +1032,10 @@ xrfragment.xrf.src = function(v, opts){
modelInstance.scale.y = mesh.scale.y modelInstance.scale.y = mesh.scale.y
modelInstance.scale.x = mesh.scale.z modelInstance.scale.x = mesh.scale.z
// now apply XR Fragments overrides from URI // now apply XR Fragments overrides from URI
// *TODO* move to a central location (pull-up) for( var i in frag )
for( var i in args ){ xrf.eval.fragment(i, Object.assign(opts,{frag, model:modelInstance,scene:sceneInstance}))
if( i == "scale" ){
console.log(" └ setting scale")
modelInstance.scale.x = args[i].x
modelInstance.scale.y = args[i].y
modelInstance.scale.z = args[i].z
}
}
// Add the instance to the scene // Add the instance to the scene
scene.add(modelInstance); model.scene.add(modelInstance);
console.dir(model)
console.dir(modelInstance)
} }
} }
window.AFRAME.registerComponent('xrf', { window.AFRAME.registerComponent('xrf', {
@ -784,9 +1044,15 @@ window.AFRAME.registerComponent('xrf', {
}, },
init: function () { init: function () {
if( !AFRAME.XRF ) this.initXRFragments() if( !AFRAME.XRF ) this.initXRFragments()
if( !this.rig && this.el.querySelector('[camera]') ) if( typeof this.data == "string" ){
AFRAME.XRF.rig = this.el AFRAME.XRF.navigate.to(this.data)
.then( (model) => {
let gets = [ ...document.querySelectorAll('[xrf-get]') ]
gets.map( (g) => g.emit('update',model) )
})
}
}, },
initXRFragments: function(){ initXRFragments: function(){
let aScene = document.querySelector('a-scene') let aScene = document.querySelector('a-scene')
// enable XR fragments // enable XR fragments
@ -796,60 +1062,54 @@ window.AFRAME.registerComponent('xrf', {
scene: aScene.object3D, scene: aScene.object3D,
renderer: aScene.renderer, renderer: aScene.renderer,
debug: true, debug: true,
loaders: [ THREE.GLTFLoader ], // which 3D assets to check for XR fragments? loaders: { gltf: THREE.GLTFLoader } // which 3D assets (exts) to check for XR fragments?
}) })
if( !XRF.camera ) throw 'xrfragment: no camera detected, please declare <a-entity camera..> ABOVE entities with xrf-attributes'
// override the 'pos' XR Fragment so we can translate the camera rig (not the camera itself) // override the camera-related XR Fragments so the camera-rig is affected
XRF.pos = (xrf,v,opts) => { let camOverride = (xrf,v,opts) => {
let { mesh, model, camera, scene, renderer, THREE} = opts opts.camera = $('[camera]').object3D //parentElement.object3D
console.log("!pos") xrf(v,opts)
camera.parent.parent.position.x = v.x
camera.parent.parent.position.y = v.y
camera.parent.parent.position.z = v.z
// xrf(v,opts) // skip threejs handler
} }
// override the 'rot' XR Fragment so we can translate the camera rig (not the camera itself) XRF.pos = camOverride
XRF.rot = (xrf,v,opts) => { XRF.rot = camOverride
let { mesh, model, camera, scene, renderer, THREE} = opts XRF.href = camOverride
camera.parent.parent.rotation.x = v.x * Math.PI / 180;
camera.parent.parent.rotation.y = v.y * Math.PI / 180;
camera.parent.parent.rotation.z = v.z * Math.PI / 180;
// xrf(v,opts) // skip threejs handler
}
}
});
AFRAME.registerComponent('gltf-to-entity', { },
})
window.AFRAME.registerComponent('xrf-get', {
schema: { schema: {
from: {default: '', type: 'selector'}, name: {type: 'string'},
name: {default: ''}, duplicate: {type: 'boolean'}
duplicate: {type:'boolean'}
}, },
init: function () { init: function () {
var el = this.el;
var data = this.data;
data.from.addEventListener('model-loaded', evt => { var el = this.el;
var model; var meshname = this.data.name || this.data;
var subset;
model = evt.detail.model; this.el.addEventListener('update', (evt) => {
console.dir(this.data.from)
subset = model.getObjectByName(data.name); let scene = evt.detail.scene
if (!subset){ let mesh = scene.getObjectByName(meshname);
console.error("Sub-object", data.name, "not found in #"+data.from.id); if (!mesh){
console.error("mesh with name '"+meshname+"' not found in model")
return; return;
} }
if( !this.data.duplicate ) subset.parent.remove(subset) if( !this.data.duplicate ) mesh.parent.remove(mesh)
let clone = subset.clone() if( this.mesh ) this.mesh.parent.remove(this.mesh) // cleanup old clone
////subset.updateMatrixWorld(); let clone = this.mesh = mesh.clone()
el.object3D.position.setFromMatrixPosition(data.from.object3D.matrixWorld); ////mesh.updateMatrixWorld();
el.object3D.quaternion.setFromRotationMatrix(data.from.object3D.matrixWorld); 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}`)
el.setObject3D('mesh', clone ); })
el.emit('model-loaded', el.getObject3D('mesh'));
});
} }
}); });

1678
dist/xrfragment.py vendored

File diff suppressed because it is too large Load Diff

View File

@ -593,114 +593,317 @@ xrfragment_XRF.isUrlOrPretypedView = new EReg("(^#|://)?\\..*","");
xrfragment_XRF.isString = new EReg(".*",""); xrfragment_XRF.isString = new EReg(".*","");
})({}); })({});
var xrfragment = $hx_exports["xrfragment"]; var xrfragment = $hx_exports["xrfragment"];
xrfragment.xrf = {} // wrapper to survive in/outside modules
xrfragment.model = {}
xrfragment.init = function(opts){ xrfragment.InteractiveGroup = function(THREE,renderer,camera){
let {
Group,
Matrix4,
Raycaster,
Vector2
} = THREE
const _pointer = new Vector2();
const _event = { type: '', data: _pointer };
class InteractiveGroup extends Group {
constructor( renderer, camera ) {
super();
if( !renderer || !camera ) return
const scope = this;
const raycaster = new Raycaster();
const tempMatrix = new Matrix4();
// Pointer Events
const element = renderer.domElement;
function onPointerEvent( event ) {
//event.stopPropagation();
const rect = renderer.domElement.getBoundingClientRect();
_pointer.x = ( event.clientX - rect.left ) / rect.width * 2 - 1;
_pointer.y = - ( event.clientY - rect.top ) / rect.height * 2 + 1;
raycaster.setFromCamera( _pointer, camera );
const intersects = raycaster.intersectObjects( scope.children, false );
if ( intersects.length > 0 ) {
const intersection = intersects[ 0 ];
const object = intersection.object;
const uv = intersection.uv;
_event.type = event.type;
_event.data.set( uv.x, 1 - uv.y );
object.dispatchEvent( _event );
}
}
element.addEventListener( 'pointerdown', onPointerEvent );
element.addEventListener( 'pointerup', onPointerEvent );
element.addEventListener( 'pointermove', onPointerEvent );
element.addEventListener( 'mousedown', onPointerEvent );
element.addEventListener( 'mouseup', onPointerEvent );
element.addEventListener( 'mousemove', onPointerEvent );
element.addEventListener( 'click', onPointerEvent );
// WebXR Controller Events
// TODO: Dispatch pointerevents too
const events = {
'move': 'mousemove',
'select': 'click',
'selectstart': 'mousedown',
'selectend': 'mouseup'
};
function onXRControllerEvent( event ) {
const controller = event.target;
tempMatrix.identity().extractRotation( controller.matrixWorld );
raycaster.ray.origin.setFromMatrixPosition( controller.matrixWorld );
raycaster.ray.direction.set( 0, 0, - 1 ).applyMatrix4( tempMatrix );
const intersections = raycaster.intersectObjects( scope.children, false );
if ( intersections.length > 0 ) {
const intersection = intersections[ 0 ];
const object = intersection.object;
const uv = intersection.uv;
_event.type = events[ event.type ];
_event.data.set( uv.x, 1 - uv.y );
if( _event.type != "mousemove" ){
console.log(event.type+" => "+_event.type)
}
object.dispatchEvent( _event );
}
}
const controller1 = renderer.xr.getController( 0 );
controller1.addEventListener( 'move', onXRControllerEvent );
controller1.addEventListener( 'select', onXRControllerEvent );
controller1.addEventListener( 'selectstart', onXRControllerEvent );
controller1.addEventListener( 'selectend', onXRControllerEvent );
const controller2 = renderer.xr.getController( 1 );
controller2.addEventListener( 'move', onXRControllerEvent );
controller2.addEventListener( 'select', onXRControllerEvent );
controller2.addEventListener( 'selectstart', onXRControllerEvent );
controller2.addEventListener( 'selectend', onXRControllerEvent );
}
}
return new InteractiveGroup(renderer,camera)
}
let xrf = xrfragment
xrf.frag = {}
xrf.model = {}
xrf.init = function(opts){
opts = opts || {} opts = opts || {}
let XRF = function(){ let XRF = function(){
alert("queries are not implemented (yet)") alert("queries are not implemented (yet)")
} }
for ( let i in opts ) xrfragment[i] = opts[i] for ( let i in opts ) xrf[i] = opts[i]
for ( let i in xrfragment.XRF ) xrfragment.XRF[i] // shortcuts to constants (NAVIGATOR e.g.) for ( let i in xrf.XRF ) xrf.XRF[i] // shortcuts to constants (NAVIGATOR e.g.)
xrfragment.Parser.debug = xrfragment.debug xrf.Parser.debug = xrf.debug
if( opts.loaders ) opts.loaders.map( xrfragment.patchLoader ) if( opts.loaders ) Object.values(opts.loaders).map( xrf.patchLoader )
xrfragment.patchRenderer(opts.renderer) xrf.patchRenderer(opts.renderer)
return xrfragment xrf.navigate.init()
return xrf
} }
xrfragment.patchRenderer = function(renderer){ xrf.patchRenderer = function(renderer){
renderer.xr.addEventListener( 'sessionstart', () => xrf.baseReferenceSpace = renderer.xr.getReferenceSpace() );
renderer.xr.enabled = true;
renderer.render = ((render) => function(scene,camera){ renderer.render = ((render) => function(scene,camera){
if( xrfragment.getLastModel() && xrfragment.getLastModel().render ) if( xrf.model && xrf.model.render )
xrfragment.getLastModel().render(scene,camera) xrf.model.render(scene,camera)
render(scene,camera) render(scene,camera)
})(renderer.render.bind(renderer)) })(renderer.render.bind(renderer))
} }
xrfragment.patchLoader = function(loader){ xrf.patchLoader = function(loader){
loader.prototype.load = ((load) => function(url, onLoad, onProgress, onError){ loader.prototype.load = ((load) => function(url, onLoad, onProgress, onError){
load.call( this, load.call( this,
url, url,
(model) => { onLoad(model); xrfragment.parseModel(model,url) }, (model) => { onLoad(model); xrf.parseModel(model,url) },
onProgress, onProgress,
onError) onError)
})(loader.prototype.load) })(loader.prototype.load)
} }
xrfragment.getFile = (url) => url.split("/").pop().replace(/#.*/,'') xrf.getFile = (url) => url.split("/").pop().replace(/#.*/,'')
xrfragment.parseModel = function(model,url){ xrf.parseModel = function(model,url){
let file = xrfragment.getFile(url) let file = xrf.getFile(url)
model.file = file model.file = file
model.render = function(){} model.render = function(){}
xrfragment.model[file] = model model.interactive = xrf.InteractiveGroup( xrf.THREE, xrf.renderer, xrf.camera)
model.scene.add(model.interactive)
console.log("scanning "+file) console.log("scanning "+file)
model.scene.traverse( (mesh) => { model.scene.traverse( (mesh) => {
console.log("◎ "+mesh.name) console.log("◎ "+ (mesh.name||`THREE.${mesh.constructor.name}`))
if( mesh.userData ){ xrf.eval.mesh(mesh,model)
let frag = {}
for( let k in mesh.userData ) xrfragment.Parser.parse( k, mesh.userData[k], frag )
for( let k in frag ){
let opts = {frag, mesh, model, camera: xrfragment.camera, scene: xrfragment.scene, renderer: xrfragment.renderer, THREE: xrfragment.THREE }
xrfragment.evalFragment(k,opts)
}
}
}) })
} }
xrfragment.evalFragment = (k, opts ) => { xrf.getLastModel = () => xrf.model.last
// call native function (xrf/env.js e.g.), or pass it to user decorator
let func = xrfragment.xrf[k] || function(){}
if( xrfragment[k] ) xrfragment[k]( func, opts.frag[k], opts)
else func( opts.frag[k], opts)
}
xrfragment.getLastModel = () => Object.values(xrfragment.model)[ Object.values(xrfragment.model).length-1 ] xrf.eval = function( url, model ){
xrfragment.eval = function( url, model ){
let notice = false let notice = false
model = model || xrfragment.getLastModel() model = model || xrf.model
let { THREE, camera } = xrfragment let { THREE, camera } = xrf
let frag = xrfragment.URI.parse( url, xrfragment.XRF.NAVIGATOR ) let frag = xrf.URI.parse( url, xrf.XRF.NAVIGATOR )
let meshes = frag.q ? [] : [camera] let meshes = frag.q ? [] : [camera]
for ( let i in meshes ) { for ( let i in meshes ) {
for ( let k in frag ){ for ( let k in frag ){
let mesh = meshes[i] let mesh = meshes[i]
if( !String(k).match(/(pos|rot)/) ) notice = true if( !String(k).match(/(pos|rot)/) ) notice = true
let opts = {frag, mesh, model, camera: xrfragment.camera, scene: xrfragment.scene, renderer: xrfragment.renderer, THREE: xrfragment.THREE } let opts = {frag, mesh, model, camera: xrf.camera, scene: xrf.scene, renderer: xrf.renderer, THREE: xrf.THREE }
xrfragment.evalFragment(k,opts) xrf.eval.fragment(k,opts)
} }
} }
if( notice ) alert("only 'pos' and 'rot' XRF.NAVIGATOR-flagged XR fragments are supported (for now)") if( notice ) alert("only 'pos' and 'rot' XRF.NAVIGATOR-flagged XR fragments are supported (for now)")
} }
xrfragment.xrf.env = function(v, opts){
xrf.eval.mesh = (mesh,model) => {
if( mesh.userData ){
let frag = {}
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 }
xrf.eval.fragment(k,opts)
}
}
}
xrf.eval.fragment = (k, opts ) => {
// call native function (xrf/env.js e.g.), or pass it to user decorator
let func = xrf.frag[k] || function(){}
if( xrf[k] ) xrf[k]( func, opts.frag[k], opts)
else func( opts.frag[k], opts)
}
xrf.reset = () => {
if( !xrf.model.scene ) return
xrf.scene.remove( xrf.model.scene )
xrf.model.scene.traverse( function(node){
if( node instanceof THREE.Mesh ){
node.geometry.dispose()
node.material.dispose()
}
})
}
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.frag.env = function(v, opts){
let { mesh, model, camera, scene, renderer, THREE} = opts let { mesh, model, camera, scene, renderer, THREE} = opts
let env = mesh.getObjectByName(v.string) let env = mesh.getObjectByName(v.string)
env.material.map.mapping = THREE.EquirectangularReflectionMapping; env.material.map.mapping = THREE.EquirectangularReflectionMapping;
scene.environment = env.material.map scene.environment = env.material.map
scene.texture = env.material.map //scene.texture = env.material.map
renderer.toneMapping = THREE.ACESFilmicToneMapping; renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure = 1; renderer.toneMappingExposure = 2;
// apply to meshes *DISABLED* renderer.environment does this
const maxAnisotropy = renderer.capabilities.getMaxAnisotropy();
setTimeout( () => {
scene.traverse( (mesh) => {
//if (mesh.material && mesh.material.map && mesh.material.metalness == 1.0) {
// mesh.material = new THREE.MeshBasicMaterial({ map: mesh.material.map });
// mesh.material.dithering = true
// mesh.material.map.anisotropy = maxAnisotropy;
// mesh.material.needsUpdate = true;
//}
//if (mesh.material && mesh.material.metalness == 1.0 ){
// mesh.material = new THREE.MeshBasicMaterial({
// color:0xffffff,
// emissive: mesh.material.map,
// envMap: env.material.map,
// side: THREE.DoubleSide,
// flatShading: true
// })
// mesh.material.needsUpdate = true
// //mesh.material.envMap = env.material.map;
// //mesh.material.envMap.intensity = 5;
// //mesh.material.needsUpdate = true;
//}
});
},500)
console.log(` └ applied image '${v.string}' as environment map`) console.log(` └ applied image '${v.string}' as environment map`)
} }
xrfragment.xrf.href = function(v, opts){ xrf.frag.href = function(v, opts){
let { mesh, model, camera, scene, renderer, THREE} = opts let { mesh, model, camera, scene, renderer, THREE} = opts
let size = 5
let texture = mesh.material.map let texture = mesh.material.map
texture.mapping = THREE.ClampToEdgeWrapping
texture.needsUpdate = true
mesh.material.dispose()
/* // poor man's equi-portal
texture.wrapS = texture.wrapT = THREE.ClampToEdgeWrapping;
mesh.material = new THREE.MeshStandardMaterial( {
envMap: texture,
roughness: 0.0,
metalness: 1,
side: THREE.DoubleSide,
})
*/
mesh.material = new THREE.ShaderMaterial( { mesh.material = new THREE.ShaderMaterial( {
side: THREE.DoubleSide, side: THREE.DoubleSide,
uniforms: { uniforms: {
@ -729,33 +932,99 @@ xrfragment.xrf.href = function(v, opts){
vec3 direction = normalize(vWorldPosition - cameraPosition); vec3 direction = normalize(vWorldPosition - cameraPosition);
vec2 sampleUV; vec2 sampleUV;
sampleUV.y = -clamp(direction.y * 0.5 + 0.5, 0.0, 1.0); sampleUV.y = -clamp(direction.y * 0.5 + 0.5, 0.0, 1.0);
sampleUV.x = atan(direction.z, -direction.x) * -RECIPROCAL_PI2 + 0.5; sampleUV.x = atan(direction.z, -direction.x) * -RECIPROCAL_PI2;
gl_FragColor = texture2D(pano, sampleUV); sampleUV.x += 0.33; // adjust focus to AFRAME's $('a-scene').components.screenshot.capture()
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);
gl_FragColor = grayscale_color;
} }
` `,
}); });
mesh.material.needsUpdate = true
const handleTeleport = (e) => {
if( mesh.clicked ) return
this.clicked = true
let portalArea = 1 // 1 meter
const meshWorldPosition = new THREE.Vector3();
meshWorldPosition.setFromMatrixPosition(mesh.matrixWorld);
const cameraDirection = new THREE.Vector3();
camera.getWorldPosition(cameraDirection);
cameraDirection.sub(meshWorldPosition);
cameraDirection.normalize();
cameraDirection.multiplyScalar(portalArea); // move away from portal
const newPos = meshWorldPosition.clone().add(cameraDirection);
const positionInFrontOfPortal = () => {
camera.position.copy(newPos);
camera.lookAt(meshWorldPosition);
if( 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 );
const teleportSpaceOffset = xrf.baseReferenceSpace.getOffsetReferenceSpace( transform );
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!
setTimeout( () => mesh.clicked = false, 200 ) // prevent double clicks
}
if( !opts.frag.q ) mesh.addEventListener('click', handleTeleport )
// lazy remove mesh (because we're inside a traverse)
setTimeout( () => {
model.interactive.add(mesh) // make clickable
},200)
} }
xrfragment.xrf.pos = function(v, opts){ xrf.frag.pos = function(v, opts){
let { frag, mesh, model, camera, scene, renderer, THREE} = opts let { frag, mesh, model, camera, scene, renderer, THREE} = opts
console.log(" └ setting camera position to "+v.string) console.log(" └ setting camera position to "+v.string)
camera.position.x = v.x camera.position.x = v.x
camera.position.y = v.y camera.position.y = v.y
camera.position.z = v.z camera.position.z = v.z
} }
xrfragment.xrf.rot = function(v, opts){ xrf.frag.q = function(v, opts){
let { frag, mesh, model, camera, scene, renderer, THREE} = opts
console.log(" └ running query ")
for ( let i in v.query ) {
let target = v.query[i]
// remove objects if requested
if( target.id != undefined && (target.mesh = scene.getObjectByName(i)) ){
target.mesh.visible = target.id
target.mesh.parent.remove(target.mesh)
console.log(` └ removing mesh: ${i}`)
}else console.log(` └ mesh not found: ${i}`)
}
}
xrf.frag.rot = function(v, opts){
let { mesh, model, camera, scene, renderer, THREE} = opts let { mesh, model, camera, scene, renderer, THREE} = opts
camera.rotation.x = v.x * Math.PI / 180; camera.rotation.x = v.x * Math.PI / 180;
camera.rotation.y = v.y * Math.PI / 180; camera.rotation.y = v.y * Math.PI / 180;
camera.rotation.z = v.z * Math.PI / 180; camera.rotation.z = v.z * Math.PI / 180;
} }
xrfragment.xrf.src = function(v, opts){ // *TODO* use webgl instancing
xrf.frag.src = function(v, opts){
let { mesh, model, camera, scene, renderer, THREE} = opts let { mesh, model, camera, scene, renderer, THREE} = opts
if( v.string[0] == "#" ){ // local if( v.string[0] == "#" ){ // local
console.log(" └ instancing src") console.log(" └ instancing src")
let args = xrfragment.URI.parse(v.string) let frag = xrfragment.URI.parse(v.string)
// Get an instance of the original model // Get an instance of the original model
const modelInstance = new THREE.Group(); const modelInstance = new THREE.Group();
modelInstance.add(model.scene.clone()); let sceneInstance = model.scene.clone()
modelInstance.add(sceneInstance)
modelInstance.position.z = mesh.position.z modelInstance.position.z = mesh.position.z
modelInstance.position.y = mesh.position.y modelInstance.position.y = mesh.position.y
modelInstance.position.x = mesh.position.x modelInstance.position.x = mesh.position.x
@ -763,19 +1032,10 @@ xrfragment.xrf.src = function(v, opts){
modelInstance.scale.y = mesh.scale.y modelInstance.scale.y = mesh.scale.y
modelInstance.scale.x = mesh.scale.z modelInstance.scale.x = mesh.scale.z
// now apply XR Fragments overrides from URI // now apply XR Fragments overrides from URI
// *TODO* move to a central location (pull-up) for( var i in frag )
for( var i in args ){ xrf.eval.fragment(i, Object.assign(opts,{frag, model:modelInstance,scene:sceneInstance}))
if( i == "scale" ){
console.log(" └ setting scale")
modelInstance.scale.x = args[i].x
modelInstance.scale.y = args[i].y
modelInstance.scale.z = args[i].z
}
}
// Add the instance to the scene // Add the instance to the scene
scene.add(modelInstance); model.scene.add(modelInstance);
console.dir(model)
console.dir(modelInstance)
} }
} }
export default xrfragment; export default xrfragment;

View File

@ -0,0 +1 @@
../../assets/example3.gltf

View File

@ -5,10 +5,10 @@
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no"> <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<link rel="stylesheet" href="./../../assets/axist.min.css" /> <link rel="stylesheet" href="./../../assets/axist.min.css" />
<link type="text/css" rel="stylesheet" href="main.css"> <link type="text/css" rel="stylesheet" href="./main.css">
<link type="text/css" rel="stylesheet" href="./../../assets/style.css"/> <link type="text/css" rel="stylesheet" href="./../../assets/style.css"/>
<script async src="./../../assets/alpine.min.js"></script> <script async src="./../../assets/alpine.min.js" defer></script>
<script src="https://aframe.io/releases/1.4.0/aframe.min.js"></script> <script src="https://aframe.io/releases/1.4.2/aframe.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/aframe-blink-controls/dist/aframe-blink-controls.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/aframe-blink-controls/dist/aframe-blink-controls.min.js"></script>
<script src="./../../../dist/xrfragment.aframe.js"></script> <script src="./../../../dist/xrfragment.aframe.js"></script>
</head> </head>
@ -26,15 +26,14 @@
<a id="source" target="_blank" href="https://github.com/coderofsalvation/xrfragment/blob/main/example/aframe/sandbox/index.html">sourcecode</a> <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 id="model" target="_blank" href="">⬇️ model</a>
<textarea style="display:none"></textarea> <textarea style="display:none"></textarea>
<a-scene> <a-scene light="defaultLightsEnabled: false">
<a-entity id="model" xrf gltf-model="./../../assets/example3.gltf" /> <a-entity xrf id="player" >
<a-entity id="floor" gltf-to-entity="from: #model; name: floor" /> <a-entity camera position="0 1.6 15" wasd-controls id="camera"></a-entity>
<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 xrf id="player"> <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 camera position="0 1.6 15" look-controls wasd-controls></a-entity>
<a-entity id="left-hand" oculus-touch-controls="hand: left" blink-controls="cameraRig:#player; teleportOrigin: #camera; collisionEntities: #floor"></a-entity>
<a-entity id="right-hand" oculus-touch-controls="hand: right" blink-controls="cameraRig:#player; teleportOrigin: #camera; collisionEntities: #floor"></a-entity>
</a-entity> </a-entity>
<a-entity id="home" xrf="example3.gltf"></a-entity>
<a-entity id="floor" xrf-get="floor"></a-entity>
</a-scene> </a-scene>
@ -43,16 +42,32 @@
window.$ = (s) => document.querySelector(s) window.$ = (s) => document.querySelector(s)
if( document.location.search.length > 2 ) if( document.location.search.length > 2 )
$('[gltf-model]').setAttribute('gltf-model', document.location.search.substr(1) ) $('#home').setAttribute('xrf', document.location.search.substr(1) )
$('a-scene').addEventListener('loaded', () => { $('a-scene').addEventListener('loaded', () => {
setupConsole( $('textarea') ) setupConsole( $('textarea') )
// init navigator url // update url when sandbox-url is updated
document.location.hash = $('#uri').value
window.addEventListener('hashchange', () => { window.addEventListener('hashchange', () => {
window.AFRAME.XRF.eval( $('#uri').value = document.location.hash ) window.AFRAME.XRF.eval( $('#uri').value = document.location.hash )
}) })
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> </script>
</body> </body>

View File

@ -0,0 +1 @@
../../assets/other.gltf

File diff suppressed because one or more lines are too long

638
example/assets/other.gltf Normal file

File diff suppressed because one or more lines are too long

View File

@ -5,7 +5,7 @@
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no"> <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<link rel="stylesheet" href="./../../assets/axist.min.css" /> <link rel="stylesheet" href="./../../assets/axist.min.css" />
<link type="text/css" rel="stylesheet" href="main.css"> <link type="text/css" rel="stylesheet" href="./main.css">
<link type="text/css" rel="stylesheet" href="./../../assets/style.css"/> <link type="text/css" rel="stylesheet" href="./../../assets/style.css"/>
</head> </head>
<body> <body>
@ -96,14 +96,14 @@
scene, scene,
renderer, renderer,
debug: true, debug: true,
loaders: [ GLTFLoader, FBXLoader ], // which 3D assets to check for XR fragments? loaders: { gltf: GLTFLoader, fbx: FBXLoader }, // which 3D assets (extensions) to check for XR fragments?
}) })
// init navigator url // init navigator url
document.location.hash = $('#uri').value
window.addEventListener('hashchange', () => { window.addEventListener('hashchange', () => {
window.XRF.eval( $('#uri').value = document.location.hash ) window.XRF.eval( $('#uri').value = document.location.hash )
}) })
if( document.location.hash.length < 2 ) document.location.hash = $('#uri').value
// optional: react/extend/hook into XR fragment // optional: react/extend/hook into XR fragment
XRF.env = (xrf,v,opts) => { XRF.env = (xrf,v,opts) => {
@ -127,35 +127,9 @@
window.XRF = XRF // expose to form window.XRF = XRF // expose to form
// load 3D asset let file = document.location.search.length > 2 ? document.location.search.substr(1) : './../../assets/example3.gltf'
let model;
const loader = new GLTFLoader().setPath( './../../assets/')
let loadGLTF = function ( gltf ) {
if( model ){
scene.remove(model)
//model.dispose()
}
const maxAnisotropy = renderer.capabilities.getMaxAnisotropy();
function recursivelySetChildrenUnlit(mesh,cb) {
cb(mesh)
if (mesh.children) {
for (var i = 0; i < mesh.children.length; i++) {
recursivelySetChildrenUnlit(mesh.children[i],cb);
}
}
}
scene.add( model = gltf.scene );
render();
};
let file = document.location.search.length > 2 ? document.location.search.substr(1) : 'example2.gltf'
$('#model').setAttribute("href","./../../asset/"+file) $('#model').setAttribute("href","./../../asset/"+file)
loader.load( file, loadGLTF ); XRF.navigate( file )
// setup mouse controls // setup mouse controls
@ -171,7 +145,12 @@
//controls.maxPolarAngle = Math.PI / 2; //controls.maxPolarAngle = Math.PI / 2;
//controls.target = new THREE.Vector3(0,1.6,0) //controls.target = new THREE.Vector3(0,1.6,0)
camera.position.set( 0, 4, 15 ); //let cameraRig = new THREE.Group()
//cameraRig.position.set( 0, 1.6, 15 );
camera.position.set( 0, 1.6, 15 );
//cameraRig.add(camera)
//cameraRig.position.set( 0, 4, 15 );
//controls.update() //controls.update()
const geometry = new THREE.BufferGeometry(); const geometry = new THREE.BufferGeometry();
@ -207,21 +186,12 @@
const gui = new GUI( { width: 300 } ); const gui = new GUI( { width: 300 } );
gui.add( parameters, 'env', 0.2, 3.0, 0.1 ).onChange( onChange ); gui.add( parameters, 'env', 0.2, 3.0, 0.1 ).onChange( onChange );
const group = new InteractiveGroup( renderer, camera );
vrbutton.addEventListener('click', () => {
// show gui inside VR scene
gui.domElement.style.visibility = 'hidden';
scene.add( group );
})
const mesh = new HTMLMesh( gui.domElement ); const mesh = new HTMLMesh( gui.domElement );
mesh.position.x = - 0.75; mesh.position.x = - 0.75;
mesh.position.y = 1.5; mesh.position.y = 1.5;
mesh.position.z = 0.3; mesh.position.z = 0.3;
mesh.rotation.y = Math.PI / 4; mesh.rotation.y = Math.PI / 4;
mesh.scale.setScalar( 2 ); mesh.scale.setScalar( 2 );
group.add( mesh );
// Add stats.js // Add stats.js
@ -236,7 +206,14 @@
statsMesh.position.z = 0.3; statsMesh.position.z = 0.3;
statsMesh.rotation.y = Math.PI / 4; statsMesh.rotation.y = Math.PI / 4;
statsMesh.scale.setScalar( 2.5 ); statsMesh.scale.setScalar( 2.5 );
group.add( statsMesh );
vrbutton.addEventListener('click', () => {
// show gui inside VR scene
gui.domElement.style.visibility = 'hidden';
XRF.interactive.add( mesh );
XRF.interactive.add( statsMesh );
scene.add( XRF.interactive );
})
let fileLoaders = loadFile({ let fileLoaders = loadFile({
".gltf": (file) => file.arrayBuffer().then( (data) => loader.parse( data, '', loadGLTF, console.error ) ), ".gltf": (file) => file.arrayBuffer().then( (data) => loader.parse( data, '', loadGLTF, console.error ) ),

View File

@ -0,0 +1 @@
../../assets/other.gltf

1
make
View File

@ -62,6 +62,7 @@ build_js(){
src/3rd/three/*.js \ src/3rd/three/*.js \
src/3rd/three/xrf/*.js \ src/3rd/three/xrf/*.js \
src/3rd/aframe/*.js > dist/xrfragment.aframe.js src/3rd/aframe/*.js > dist/xrfragment.aframe.js
ls -la dist | grep js
exit $ok exit $ok
} }

View File

@ -4,9 +4,15 @@ window.AFRAME.registerComponent('xrf', {
}, },
init: function () { init: function () {
if( !AFRAME.XRF ) this.initXRFragments() if( !AFRAME.XRF ) this.initXRFragments()
if( !this.rig && this.el.querySelector('[camera]') ) if( typeof this.data == "string" ){
AFRAME.XRF.rig = this.el AFRAME.XRF.navigate.to(this.data)
.then( (model) => {
let gets = [ ...document.querySelectorAll('[xrf-get]') ]
gets.map( (g) => g.emit('update',model) )
})
}
}, },
initXRFragments: function(){ initXRFragments: function(){
let aScene = document.querySelector('a-scene') let aScene = document.querySelector('a-scene')
// enable XR fragments // enable XR fragments
@ -16,60 +22,54 @@ window.AFRAME.registerComponent('xrf', {
scene: aScene.object3D, scene: aScene.object3D,
renderer: aScene.renderer, renderer: aScene.renderer,
debug: true, debug: true,
loaders: [ THREE.GLTFLoader ], // which 3D assets to check for XR fragments? loaders: { gltf: THREE.GLTFLoader } // which 3D assets (exts) to check for XR fragments?
}) })
if( !XRF.camera ) throw 'xrfragment: no camera detected, please declare <a-entity camera..> ABOVE entities with xrf-attributes'
// override the 'pos' XR Fragment so we can translate the camera rig (not the camera itself) // override the camera-related XR Fragments so the camera-rig is affected
XRF.pos = (xrf,v,opts) => { let camOverride = (xrf,v,opts) => {
let { mesh, model, camera, scene, renderer, THREE} = opts opts.camera = $('[camera]').object3D //parentElement.object3D
console.log("!pos") xrf(v,opts)
camera.parent.parent.position.x = v.x
camera.parent.parent.position.y = v.y
camera.parent.parent.position.z = v.z
// xrf(v,opts) // skip threejs handler
} }
// override the 'rot' XR Fragment so we can translate the camera rig (not the camera itself) XRF.pos = camOverride
XRF.rot = (xrf,v,opts) => { XRF.rot = camOverride
let { mesh, model, camera, scene, renderer, THREE} = opts XRF.href = camOverride
camera.parent.parent.rotation.x = v.x * Math.PI / 180;
camera.parent.parent.rotation.y = v.y * Math.PI / 180;
camera.parent.parent.rotation.z = v.z * Math.PI / 180;
// xrf(v,opts) // skip threejs handler
}
}
});
AFRAME.registerComponent('gltf-to-entity', { },
})
window.AFRAME.registerComponent('xrf-get', {
schema: { schema: {
from: {default: '', type: 'selector'}, name: {type: 'string'},
name: {default: ''}, duplicate: {type: 'boolean'}
duplicate: {type:'boolean'}
}, },
init: function () { init: function () {
var el = this.el;
var data = this.data;
data.from.addEventListener('model-loaded', evt => { var el = this.el;
var model; var meshname = this.data.name || this.data;
var subset;
model = evt.detail.model; this.el.addEventListener('update', (evt) => {
console.dir(this.data.from)
subset = model.getObjectByName(data.name); let scene = evt.detail.scene
if (!subset){ let mesh = scene.getObjectByName(meshname);
console.error("Sub-object", data.name, "not found in #"+data.from.id); if (!mesh){
console.error("mesh with name '"+meshname+"' not found in model")
return; return;
} }
if( !this.data.duplicate ) subset.parent.remove(subset) if( !this.data.duplicate ) mesh.parent.remove(mesh)
let clone = subset.clone() if( this.mesh ) this.mesh.parent.remove(this.mesh) // cleanup old clone
////subset.updateMatrixWorld(); let clone = this.mesh = mesh.clone()
el.object3D.position.setFromMatrixPosition(data.from.object3D.matrixWorld); ////mesh.updateMatrixWorld();
el.object3D.quaternion.setFromRotationMatrix(data.from.object3D.matrixWorld); 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}`)
el.setObject3D('mesh', clone ); })
el.emit('model-loaded', el.getObject3D('mesh'));
});
} }
}); });

View File

@ -0,0 +1,126 @@
// wrapper to survive in/outside modules
xrfragment.InteractiveGroup = function(THREE,renderer,camera){
let {
Group,
Matrix4,
Raycaster,
Vector2
} = THREE
const _pointer = new Vector2();
const _event = { type: '', data: _pointer };
class InteractiveGroup extends Group {
constructor( renderer, camera ) {
super();
if( !renderer || !camera ) return
const scope = this;
const raycaster = new Raycaster();
const tempMatrix = new Matrix4();
// Pointer Events
const element = renderer.domElement;
function onPointerEvent( event ) {
//event.stopPropagation();
const rect = renderer.domElement.getBoundingClientRect();
_pointer.x = ( event.clientX - rect.left ) / rect.width * 2 - 1;
_pointer.y = - ( event.clientY - rect.top ) / rect.height * 2 + 1;
raycaster.setFromCamera( _pointer, camera );
const intersects = raycaster.intersectObjects( scope.children, false );
if ( intersects.length > 0 ) {
const intersection = intersects[ 0 ];
const object = intersection.object;
const uv = intersection.uv;
_event.type = event.type;
_event.data.set( uv.x, 1 - uv.y );
object.dispatchEvent( _event );
}
}
element.addEventListener( 'pointerdown', onPointerEvent );
element.addEventListener( 'pointerup', onPointerEvent );
element.addEventListener( 'pointermove', onPointerEvent );
element.addEventListener( 'mousedown', onPointerEvent );
element.addEventListener( 'mouseup', onPointerEvent );
element.addEventListener( 'mousemove', onPointerEvent );
element.addEventListener( 'click', onPointerEvent );
// WebXR Controller Events
// TODO: Dispatch pointerevents too
const events = {
'move': 'mousemove',
'select': 'click',
'selectstart': 'mousedown',
'selectend': 'mouseup'
};
function onXRControllerEvent( event ) {
const controller = event.target;
tempMatrix.identity().extractRotation( controller.matrixWorld );
raycaster.ray.origin.setFromMatrixPosition( controller.matrixWorld );
raycaster.ray.direction.set( 0, 0, - 1 ).applyMatrix4( tempMatrix );
const intersections = raycaster.intersectObjects( scope.children, false );
if ( intersections.length > 0 ) {
const intersection = intersections[ 0 ];
const object = intersection.object;
const uv = intersection.uv;
_event.type = events[ event.type ];
_event.data.set( uv.x, 1 - uv.y );
if( _event.type != "mousemove" ){
console.log(event.type+" => "+_event.type)
}
object.dispatchEvent( _event );
}
}
const controller1 = renderer.xr.getController( 0 );
controller1.addEventListener( 'move', onXRControllerEvent );
controller1.addEventListener( 'select', onXRControllerEvent );
controller1.addEventListener( 'selectstart', onXRControllerEvent );
controller1.addEventListener( 'selectend', onXRControllerEvent );
const controller2 = renderer.xr.getController( 1 );
controller2.addEventListener( 'move', onXRControllerEvent );
controller2.addEventListener( 'select', onXRControllerEvent );
controller2.addEventListener( 'selectstart', onXRControllerEvent );
controller2.addEventListener( 'selectend', onXRControllerEvent );
}
}
return new InteractiveGroup(renderer,camera)
}

View File

@ -1,82 +1,141 @@
xrfragment.xrf = {} let xrf = xrfragment
xrfragment.model = {} xrf.frag = {}
xrf.model = {}
xrfragment.init = function(opts){ xrf.init = function(opts){
opts = opts || {} opts = opts || {}
let XRF = function(){ let XRF = function(){
alert("queries are not implemented (yet)") alert("queries are not implemented (yet)")
} }
for ( let i in opts ) xrfragment[i] = opts[i] for ( let i in opts ) xrf[i] = opts[i]
for ( let i in xrfragment.XRF ) xrfragment.XRF[i] // shortcuts to constants (NAVIGATOR e.g.) for ( let i in xrf.XRF ) xrf.XRF[i] // shortcuts to constants (NAVIGATOR e.g.)
xrfragment.Parser.debug = xrfragment.debug xrf.Parser.debug = xrf.debug
if( opts.loaders ) opts.loaders.map( xrfragment.patchLoader ) if( opts.loaders ) Object.values(opts.loaders).map( xrf.patchLoader )
xrfragment.patchRenderer(opts.renderer) xrf.patchRenderer(opts.renderer)
return xrfragment xrf.navigate.init()
return xrf
} }
xrfragment.patchRenderer = function(renderer){ xrf.patchRenderer = function(renderer){
renderer.xr.addEventListener( 'sessionstart', () => xrf.baseReferenceSpace = renderer.xr.getReferenceSpace() );
renderer.xr.enabled = true;
renderer.render = ((render) => function(scene,camera){ renderer.render = ((render) => function(scene,camera){
if( xrfragment.getLastModel() && xrfragment.getLastModel().render ) if( xrf.model && xrf.model.render )
xrfragment.getLastModel().render(scene,camera) xrf.model.render(scene,camera)
render(scene,camera) render(scene,camera)
})(renderer.render.bind(renderer)) })(renderer.render.bind(renderer))
} }
xrfragment.patchLoader = function(loader){ xrf.patchLoader = function(loader){
loader.prototype.load = ((load) => function(url, onLoad, onProgress, onError){ loader.prototype.load = ((load) => function(url, onLoad, onProgress, onError){
load.call( this, load.call( this,
url, url,
(model) => { onLoad(model); xrfragment.parseModel(model,url) }, (model) => { onLoad(model); xrf.parseModel(model,url) },
onProgress, onProgress,
onError) onError)
})(loader.prototype.load) })(loader.prototype.load)
} }
xrfragment.getFile = (url) => url.split("/").pop().replace(/#.*/,'') xrf.getFile = (url) => url.split("/").pop().replace(/#.*/,'')
xrfragment.parseModel = function(model,url){ xrf.parseModel = function(model,url){
let file = xrfragment.getFile(url) let file = xrf.getFile(url)
model.file = file model.file = file
model.render = function(){} model.render = function(){}
xrfragment.model[file] = model model.interactive = xrf.InteractiveGroup( xrf.THREE, xrf.renderer, xrf.camera)
model.scene.add(model.interactive)
console.log("scanning "+file) console.log("scanning "+file)
model.scene.traverse( (mesh) => { model.scene.traverse( (mesh) => {
console.log("◎ "+mesh.name) console.log("◎ "+ (mesh.name||`THREE.${mesh.constructor.name}`))
if( mesh.userData ){ xrf.eval.mesh(mesh,model)
let frag = {}
for( let k in mesh.userData ) xrfragment.Parser.parse( k, mesh.userData[k], frag )
for( let k in frag ){
let opts = {frag, mesh, model, camera: xrfragment.camera, scene: xrfragment.scene, renderer: xrfragment.renderer, THREE: xrfragment.THREE }
xrfragment.evalFragment(k,opts)
}
}
}) })
} }
xrfragment.evalFragment = (k, opts ) => { xrf.getLastModel = () => xrf.model.last
// call native function (xrf/env.js e.g.), or pass it to user decorator
let func = xrfragment.xrf[k] || function(){}
if( xrfragment[k] ) xrfragment[k]( func, opts.frag[k], opts)
else func( opts.frag[k], opts)
}
xrfragment.getLastModel = () => Object.values(xrfragment.model)[ Object.values(xrfragment.model).length-1 ] xrf.eval = function( url, model ){
xrfragment.eval = function( url, model ){
let notice = false let notice = false
model = model || xrfragment.getLastModel() model = model || xrf.model
let { THREE, camera } = xrfragment let { THREE, camera } = xrf
let frag = xrfragment.URI.parse( url, xrfragment.XRF.NAVIGATOR ) let frag = xrf.URI.parse( url, xrf.XRF.NAVIGATOR )
let meshes = frag.q ? [] : [camera] let meshes = frag.q ? [] : [camera]
for ( let i in meshes ) { for ( let i in meshes ) {
for ( let k in frag ){ for ( let k in frag ){
let mesh = meshes[i] let mesh = meshes[i]
if( !String(k).match(/(pos|rot)/) ) notice = true if( !String(k).match(/(pos|rot)/) ) notice = true
let opts = {frag, mesh, model, camera: xrfragment.camera, scene: xrfragment.scene, renderer: xrfragment.renderer, THREE: xrfragment.THREE } let opts = {frag, mesh, model, camera: xrf.camera, scene: xrf.scene, renderer: xrf.renderer, THREE: xrf.THREE }
xrfragment.evalFragment(k,opts) xrf.eval.fragment(k,opts)
} }
} }
if( notice ) alert("only 'pos' and 'rot' XRF.NAVIGATOR-flagged XR fragments are supported (for now)") if( notice ) alert("only 'pos' and 'rot' XRF.NAVIGATOR-flagged XR fragments are supported (for now)")
} }
xrf.eval.mesh = (mesh,model) => {
if( mesh.userData ){
let frag = {}
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 }
xrf.eval.fragment(k,opts)
}
}
}
xrf.eval.fragment = (k, opts ) => {
// call native function (xrf/env.js e.g.), or pass it to user decorator
let func = xrf.frag[k] || function(){}
if( xrf[k] ) xrf[k]( func, opts.frag[k], opts)
else func( opts.frag[k], opts)
}
xrf.reset = () => {
if( !xrf.model.scene ) return
xrf.scene.remove( xrf.model.scene )
xrf.model.scene.traverse( function(node){
if( node instanceof THREE.Mesh ){
node.geometry.dispose()
node.material.dispose()
}
})
}
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}` )
}

View File

@ -1,10 +1,35 @@
xrfragment.xrf.env = function(v, opts){ xrf.frag.env = function(v, opts){
let { mesh, model, camera, scene, renderer, THREE} = opts let { mesh, model, camera, scene, renderer, THREE} = opts
let env = mesh.getObjectByName(v.string) let env = mesh.getObjectByName(v.string)
env.material.map.mapping = THREE.EquirectangularReflectionMapping; env.material.map.mapping = THREE.EquirectangularReflectionMapping;
scene.environment = env.material.map scene.environment = env.material.map
scene.texture = env.material.map //scene.texture = env.material.map
renderer.toneMapping = THREE.ACESFilmicToneMapping; renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure = 1; renderer.toneMappingExposure = 2;
// apply to meshes *DISABLED* renderer.environment does this
const maxAnisotropy = renderer.capabilities.getMaxAnisotropy();
setTimeout( () => {
scene.traverse( (mesh) => {
//if (mesh.material && mesh.material.map && mesh.material.metalness == 1.0) {
// mesh.material = new THREE.MeshBasicMaterial({ map: mesh.material.map });
// mesh.material.dithering = true
// mesh.material.map.anisotropy = maxAnisotropy;
// mesh.material.needsUpdate = true;
//}
//if (mesh.material && mesh.material.metalness == 1.0 ){
// mesh.material = new THREE.MeshBasicMaterial({
// color:0xffffff,
// emissive: mesh.material.map,
// envMap: env.material.map,
// side: THREE.DoubleSide,
// flatShading: true
// })
// mesh.material.needsUpdate = true
// //mesh.material.envMap = env.material.map;
// //mesh.material.envMap.intensity = 5;
// //mesh.material.needsUpdate = true;
//}
});
},500)
console.log(` └ applied image '${v.string}' as environment map`) console.log(` └ applied image '${v.string}' as environment map`)
} }

View File

@ -1,19 +1,12 @@
xrfragment.xrf.href = function(v, opts){ xrf.frag.href = function(v, opts){
let { mesh, model, camera, scene, renderer, THREE} = opts let { mesh, model, camera, scene, renderer, THREE} = opts
let size = 5
let texture = mesh.material.map let texture = mesh.material.map
texture.mapping = THREE.ClampToEdgeWrapping
texture.needsUpdate = true
mesh.material.dispose()
/* // poor man's equi-portal
texture.wrapS = texture.wrapT = THREE.ClampToEdgeWrapping;
mesh.material = new THREE.MeshStandardMaterial( {
envMap: texture,
roughness: 0.0,
metalness: 1,
side: THREE.DoubleSide,
})
*/
mesh.material = new THREE.ShaderMaterial( { mesh.material = new THREE.ShaderMaterial( {
side: THREE.DoubleSide, side: THREE.DoubleSide,
uniforms: { uniforms: {
@ -42,9 +35,58 @@ xrfragment.xrf.href = function(v, opts){
vec3 direction = normalize(vWorldPosition - cameraPosition); vec3 direction = normalize(vWorldPosition - cameraPosition);
vec2 sampleUV; vec2 sampleUV;
sampleUV.y = -clamp(direction.y * 0.5 + 0.5, 0.0, 1.0); sampleUV.y = -clamp(direction.y * 0.5 + 0.5, 0.0, 1.0);
sampleUV.x = atan(direction.z, -direction.x) * -RECIPROCAL_PI2 + 0.5; sampleUV.x = atan(direction.z, -direction.x) * -RECIPROCAL_PI2;
gl_FragColor = texture2D(pano, sampleUV); sampleUV.x += 0.33; // adjust focus to AFRAME's $('a-scene').components.screenshot.capture()
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);
gl_FragColor = grayscale_color;
} }
` `,
}); });
mesh.material.needsUpdate = true
const handleTeleport = (e) => {
if( mesh.clicked ) return
this.clicked = true
let portalArea = 1 // 1 meter
const meshWorldPosition = new THREE.Vector3();
meshWorldPosition.setFromMatrixPosition(mesh.matrixWorld);
const cameraDirection = new THREE.Vector3();
camera.getWorldPosition(cameraDirection);
cameraDirection.sub(meshWorldPosition);
cameraDirection.normalize();
cameraDirection.multiplyScalar(portalArea); // move away from portal
const newPos = meshWorldPosition.clone().add(cameraDirection);
const positionInFrontOfPortal = () => {
camera.position.copy(newPos);
camera.lookAt(meshWorldPosition);
if( 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 );
const teleportSpaceOffset = xrf.baseReferenceSpace.getOffsetReferenceSpace( transform );
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!
setTimeout( () => mesh.clicked = false, 200 ) // prevent double clicks
}
if( !opts.frag.q ) mesh.addEventListener('click', handleTeleport )
// lazy remove mesh (because we're inside a traverse)
setTimeout( () => {
model.interactive.add(mesh) // make clickable
},200)
} }

View File

@ -1,4 +1,4 @@
xrfragment.xrf.pos = function(v, opts){ xrf.frag.pos = function(v, opts){
let { frag, mesh, model, camera, scene, renderer, THREE} = opts let { frag, mesh, model, camera, scene, renderer, THREE} = opts
console.log(" └ setting camera position to "+v.string) console.log(" └ setting camera position to "+v.string)
camera.position.x = v.x camera.position.x = v.x

14
src/3rd/three/xrf/q.js Normal file
View File

@ -0,0 +1,14 @@
xrf.frag.q = function(v, opts){
let { frag, mesh, model, camera, scene, renderer, THREE} = opts
console.log(" └ running query ")
for ( let i in v.query ) {
let target = v.query[i]
// remove objects if requested
if( target.id != undefined && (target.mesh = scene.getObjectByName(i)) ){
target.mesh.visible = target.id
target.mesh.parent.remove(target.mesh)
console.log(` └ removing mesh: ${i}`)
}else console.log(` └ mesh not found: ${i}`)
}
}

View File

@ -1,4 +1,4 @@
xrfragment.xrf.rot = function(v, opts){ xrf.frag.rot = function(v, opts){
let { mesh, model, camera, scene, renderer, THREE} = opts let { mesh, model, camera, scene, renderer, THREE} = opts
camera.rotation.x = v.x * Math.PI / 180; camera.rotation.x = v.x * Math.PI / 180;
camera.rotation.y = v.y * Math.PI / 180; camera.rotation.y = v.y * Math.PI / 180;

View File

@ -1,11 +1,14 @@
xrfragment.xrf.src = function(v, opts){ // *TODO* use webgl instancing
xrf.frag.src = function(v, opts){
let { mesh, model, camera, scene, renderer, THREE} = opts let { mesh, model, camera, scene, renderer, THREE} = opts
if( v.string[0] == "#" ){ // local if( v.string[0] == "#" ){ // local
console.log(" └ instancing src") console.log(" └ instancing src")
let args = xrfragment.URI.parse(v.string) let frag = xrfragment.URI.parse(v.string)
// Get an instance of the original model // Get an instance of the original model
const modelInstance = new THREE.Group(); const modelInstance = new THREE.Group();
modelInstance.add(model.scene.clone()); let sceneInstance = model.scene.clone()
modelInstance.add(sceneInstance)
modelInstance.position.z = mesh.position.z modelInstance.position.z = mesh.position.z
modelInstance.position.y = mesh.position.y modelInstance.position.y = mesh.position.y
modelInstance.position.x = mesh.position.x modelInstance.position.x = mesh.position.x
@ -13,18 +16,9 @@ xrfragment.xrf.src = function(v, opts){
modelInstance.scale.y = mesh.scale.y modelInstance.scale.y = mesh.scale.y
modelInstance.scale.x = mesh.scale.z modelInstance.scale.x = mesh.scale.z
// now apply XR Fragments overrides from URI // now apply XR Fragments overrides from URI
// *TODO* move to a central location (pull-up) for( var i in frag )
for( var i in args ){ xrf.eval.fragment(i, Object.assign(opts,{frag, model:modelInstance,scene:sceneInstance}))
if( i == "scale" ){
console.log(" └ setting scale")
modelInstance.scale.x = args[i].x
modelInstance.scale.y = args[i].y
modelInstance.scale.z = args[i].z
}
}
// Add the instance to the scene // Add the instance to the scene
scene.add(modelInstance); model.scene.add(modelInstance);
console.dir(model)
console.dir(modelInstance)
} }
} }