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> <F9> :!./make doc<CR>
noremap <silent> <F9> :!./make build_js<CR>
noremap <silent> <F10> :!./make && echo OK && ./make tests<CR>
noremap <silent> <F11> :!./make tests \| less<CR>

View File

@ -593,114 +593,317 @@ xrfragment_XRF.isUrlOrPretypedView = new EReg("(^#|://)?\\..*","");
xrfragment_XRF.isString = new EReg(".*","");
})({});
var xrfragment = $hx_exports["xrfragment"];
xrfragment.xrf = {}
xrfragment.model = {}
// wrapper to survive in/outside modules
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 || {}
let XRF = function(){
alert("queries are not implemented (yet)")
}
for ( let i in opts ) xrfragment[i] = opts[i]
for ( let i in xrfragment.XRF ) xrfragment.XRF[i] // shortcuts to constants (NAVIGATOR e.g.)
xrfragment.Parser.debug = xrfragment.debug
if( opts.loaders ) opts.loaders.map( xrfragment.patchLoader )
xrfragment.patchRenderer(opts.renderer)
return xrfragment
for ( let i in opts ) xrf[i] = opts[i]
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.patchRenderer(opts.renderer)
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){
if( xrfragment.getLastModel() && xrfragment.getLastModel().render )
xrfragment.getLastModel().render(scene,camera)
if( xrf.model && xrf.model.render )
xrf.model.render(scene,camera)
render(scene,camera)
})(renderer.render.bind(renderer))
}
xrfragment.patchLoader = function(loader){
xrf.patchLoader = function(loader){
loader.prototype.load = ((load) => function(url, onLoad, onProgress, onError){
load.call( this,
url,
(model) => { onLoad(model); xrfragment.parseModel(model,url) },
(model) => { onLoad(model); xrf.parseModel(model,url) },
onProgress,
onError)
})(loader.prototype.load)
}
xrfragment.getFile = (url) => url.split("/").pop().replace(/#.*/,'')
xrf.getFile = (url) => url.split("/").pop().replace(/#.*/,'')
xrfragment.parseModel = function(model,url){
let file = xrfragment.getFile(url)
xrf.parseModel = function(model,url){
let file = xrf.getFile(url)
model.file = file
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)
model.scene.traverse( (mesh) => {
console.log("◎ "+mesh.name)
if( mesh.userData ){
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)
}
}
console.log("◎ "+ (mesh.name||`THREE.${mesh.constructor.name}`))
xrf.eval.mesh(mesh,model)
})
}
xrfragment.evalFragment = (k, opts ) => {
// 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.getLastModel = () => xrf.model.last
xrfragment.eval = function( url, model ){
xrf.eval = function( url, model ){
let notice = false
model = model || xrfragment.getLastModel()
let { THREE, camera } = xrfragment
let frag = xrfragment.URI.parse( url, xrfragment.XRF.NAVIGATOR )
model = model || xrf.model
let { THREE, camera } = xrf
let frag = xrf.URI.parse( url, xrf.XRF.NAVIGATOR )
let meshes = frag.q ? [] : [camera]
for ( let i in meshes ) {
for ( let k in frag ){
let mesh = meshes[i]
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 }
xrfragment.evalFragment(k,opts)
let opts = {frag, mesh, model, camera: xrf.camera, scene: xrf.scene, renderer: xrf.renderer, THREE: xrf.THREE }
xrf.eval.fragment(k,opts)
}
}
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 env = mesh.getObjectByName(v.string)
env.material.map.mapping = THREE.EquirectangularReflectionMapping;
scene.environment = env.material.map
scene.texture = env.material.map
//scene.texture = env.material.map
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`)
}
xrfragment.xrf.href = function(v, opts){
xrf.frag.href = function(v, opts){
let { mesh, model, camera, scene, renderer, THREE} = opts
let size = 5
let texture = mesh.material.map
texture.mapping = THREE.ClampToEdgeWrapping
texture.needsUpdate = true
mesh.material.dispose()
/*
texture.wrapS = texture.wrapT = THREE.ClampToEdgeWrapping;
mesh.material = new THREE.MeshStandardMaterial( {
envMap: texture,
roughness: 0.0,
metalness: 1,
side: THREE.DoubleSide,
})
*/
// poor man's equi-portal
mesh.material = new THREE.ShaderMaterial( {
side: THREE.DoubleSide,
uniforms: {
@ -729,33 +932,99 @@ xrfragment.xrf.href = function(v, opts){
vec3 direction = normalize(vWorldPosition - cameraPosition);
vec2 sampleUV;
sampleUV.y = -clamp(direction.y * 0.5 + 0.5, 0.0, 1.0);
sampleUV.x = atan(direction.z, -direction.x) * -RECIPROCAL_PI2 + 0.5;
gl_FragColor = texture2D(pano, sampleUV);
sampleUV.x = atan(direction.z, -direction.x) * -RECIPROCAL_PI2;
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
console.log(" └ setting camera position to "+v.string)
camera.position.x = v.x
camera.position.y = v.y
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
camera.rotation.x = v.x * Math.PI / 180;
camera.rotation.y = v.y * 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
if( v.string[0] == "#" ){ // local
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
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.y = mesh.position.y
modelInstance.position.x = mesh.position.x
@ -763,19 +1032,10 @@ xrfragment.xrf.src = function(v, opts){
modelInstance.scale.y = mesh.scale.y
modelInstance.scale.x = mesh.scale.z
// now apply XR Fragments overrides from URI
// *TODO* move to a central location (pull-up)
for( var i in args ){
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
}
}
for( var i in frag )
xrf.eval.fragment(i, Object.assign(opts,{frag, model:modelInstance,scene:sceneInstance}))
// Add the instance to the scene
scene.add(modelInstance);
console.dir(model)
console.dir(modelInstance)
model.scene.add(modelInstance);
}
}
window.AFRAME.registerComponent('xrf', {
@ -784,72 +1044,72 @@ window.AFRAME.registerComponent('xrf', {
},
init: function () {
if( !AFRAME.XRF ) this.initXRFragments()
if( !this.rig && this.el.querySelector('[camera]') )
AFRAME.XRF.rig = this.el
if( typeof this.data == "string" ){
AFRAME.XRF.navigate.to(this.data)
.then( (model) => {
let gets = [ ...document.querySelectorAll('[xrf-get]') ]
gets.map( (g) => g.emit('update',model) )
})
}
},
initXRFragments: function(){
let aScene = document.querySelector('a-scene')
// enable XR fragments
let XRF = AFRAME.XRF = xrfragment.init({
THREE,
camera: aScene.camera,
camera: aScene.camera,
scene: aScene.object3D,
renderer: aScene.renderer,
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)
XRF.pos = (xrf,v,opts) => {
let { mesh, model, camera, scene, renderer, THREE} = opts
console.log("!pos")
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 camera-related XR Fragments so the camera-rig is affected
let camOverride = (xrf,v,opts) => {
opts.camera = $('[camera]').object3D //parentElement.object3D
xrf(v,opts)
}
XRF.pos = camOverride
XRF.rot = camOverride
XRF.href = camOverride
// override the 'rot' XR Fragment so we can translate the camera rig (not the camera itself)
XRF.rot = (xrf,v,opts) => {
let { mesh, model, camera, scene, renderer, THREE} = opts
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: {
from: {default: '', type: 'selector'},
name: {default: ''},
duplicate: {type:'boolean'}
name: {type: 'string'},
duplicate: {type: 'boolean'}
},
init: function () {
var el = this.el;
var data = this.data;
data.from.addEventListener('model-loaded', evt => {
var model;
var subset;
model = evt.detail.model;
console.dir(this.data.from)
subset = model.getObjectByName(data.name);
if (!subset){
console.error("Sub-object", data.name, "not found in #"+data.from.id);
var el = this.el;
var meshname = this.data.name || this.data;
this.el.addEventListener('update', (evt) => {
let scene = evt.detail.scene
let mesh = scene.getObjectByName(meshname);
if (!mesh){
console.error("mesh with name '"+meshname+"' not found in model")
return;
}
if( !this.data.duplicate ) subset.parent.remove(subset)
let clone = subset.clone()
////subset.updateMatrixWorld();
el.object3D.position.setFromMatrixPosition(data.from.object3D.matrixWorld);
el.object3D.quaternion.setFromRotationMatrix(data.from.object3D.matrixWorld);
el.setObject3D('mesh', clone );
el.emit('model-loaded', el.getObject3D('mesh'));
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()
////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}`)
})
});
}
});

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(".*","");
})({});
var xrfragment = $hx_exports["xrfragment"];
xrfragment.xrf = {}
xrfragment.model = {}
// wrapper to survive in/outside modules
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 || {}
let XRF = function(){
alert("queries are not implemented (yet)")
}
for ( let i in opts ) xrfragment[i] = opts[i]
for ( let i in xrfragment.XRF ) xrfragment.XRF[i] // shortcuts to constants (NAVIGATOR e.g.)
xrfragment.Parser.debug = xrfragment.debug
if( opts.loaders ) opts.loaders.map( xrfragment.patchLoader )
xrfragment.patchRenderer(opts.renderer)
return xrfragment
for ( let i in opts ) xrf[i] = opts[i]
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.patchRenderer(opts.renderer)
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){
if( xrfragment.getLastModel() && xrfragment.getLastModel().render )
xrfragment.getLastModel().render(scene,camera)
if( xrf.model && xrf.model.render )
xrf.model.render(scene,camera)
render(scene,camera)
})(renderer.render.bind(renderer))
}
xrfragment.patchLoader = function(loader){
xrf.patchLoader = function(loader){
loader.prototype.load = ((load) => function(url, onLoad, onProgress, onError){
load.call( this,
url,
(model) => { onLoad(model); xrfragment.parseModel(model,url) },
(model) => { onLoad(model); xrf.parseModel(model,url) },
onProgress,
onError)
})(loader.prototype.load)
}
xrfragment.getFile = (url) => url.split("/").pop().replace(/#.*/,'')
xrf.getFile = (url) => url.split("/").pop().replace(/#.*/,'')
xrfragment.parseModel = function(model,url){
let file = xrfragment.getFile(url)
xrf.parseModel = function(model,url){
let file = xrf.getFile(url)
model.file = file
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)
model.scene.traverse( (mesh) => {
console.log("◎ "+mesh.name)
if( mesh.userData ){
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)
}
}
console.log("◎ "+ (mesh.name||`THREE.${mesh.constructor.name}`))
xrf.eval.mesh(mesh,model)
})
}
xrfragment.evalFragment = (k, opts ) => {
// 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.getLastModel = () => xrf.model.last
xrfragment.eval = function( url, model ){
xrf.eval = function( url, model ){
let notice = false
model = model || xrfragment.getLastModel()
let { THREE, camera } = xrfragment
let frag = xrfragment.URI.parse( url, xrfragment.XRF.NAVIGATOR )
model = model || xrf.model
let { THREE, camera } = xrf
let frag = xrf.URI.parse( url, xrf.XRF.NAVIGATOR )
let meshes = frag.q ? [] : [camera]
for ( let i in meshes ) {
for ( let k in frag ){
let mesh = meshes[i]
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 }
xrfragment.evalFragment(k,opts)
let opts = {frag, mesh, model, camera: xrf.camera, scene: xrf.scene, renderer: xrf.renderer, THREE: xrf.THREE }
xrf.eval.fragment(k,opts)
}
}
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 env = mesh.getObjectByName(v.string)
env.material.map.mapping = THREE.EquirectangularReflectionMapping;
scene.environment = env.material.map
scene.texture = env.material.map
//scene.texture = env.material.map
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`)
}
xrfragment.xrf.href = function(v, opts){
xrf.frag.href = function(v, opts){
let { mesh, model, camera, scene, renderer, THREE} = opts
let size = 5
let texture = mesh.material.map
texture.mapping = THREE.ClampToEdgeWrapping
texture.needsUpdate = true
mesh.material.dispose()
/*
texture.wrapS = texture.wrapT = THREE.ClampToEdgeWrapping;
mesh.material = new THREE.MeshStandardMaterial( {
envMap: texture,
roughness: 0.0,
metalness: 1,
side: THREE.DoubleSide,
})
*/
// poor man's equi-portal
mesh.material = new THREE.ShaderMaterial( {
side: THREE.DoubleSide,
uniforms: {
@ -729,33 +932,99 @@ xrfragment.xrf.href = function(v, opts){
vec3 direction = normalize(vWorldPosition - cameraPosition);
vec2 sampleUV;
sampleUV.y = -clamp(direction.y * 0.5 + 0.5, 0.0, 1.0);
sampleUV.x = atan(direction.z, -direction.x) * -RECIPROCAL_PI2 + 0.5;
gl_FragColor = texture2D(pano, sampleUV);
sampleUV.x = atan(direction.z, -direction.x) * -RECIPROCAL_PI2;
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
console.log(" └ setting camera position to "+v.string)
camera.position.x = v.x
camera.position.y = v.y
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
camera.rotation.x = v.x * Math.PI / 180;
camera.rotation.y = v.y * 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
if( v.string[0] == "#" ){ // local
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
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.y = mesh.position.y
modelInstance.position.x = mesh.position.x
@ -763,19 +1032,10 @@ xrfragment.xrf.src = function(v, opts){
modelInstance.scale.y = mesh.scale.y
modelInstance.scale.x = mesh.scale.z
// now apply XR Fragments overrides from URI
// *TODO* move to a central location (pull-up)
for( var i in args ){
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
}
}
for( var i in frag )
xrf.eval.fragment(i, Object.assign(opts,{frag, model:modelInstance,scene:sceneInstance}))
// Add the instance to the scene
scene.add(modelInstance);
console.dir(model)
console.dir(modelInstance)
model.scene.add(modelInstance);
}
}
export default xrfragment;

View File

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

View File

@ -5,10 +5,10 @@
<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="./main.css">
<link type="text/css" rel="stylesheet" href="./../../assets/style.css"/>
<script async src="./../../assets/alpine.min.js"></script>
<script src="https://aframe.io/releases/1.4.0/aframe.min.js"></script>
<script async src="./../../assets/alpine.min.js" defer></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="./../../../dist/xrfragment.aframe.js"></script>
</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="model" target="_blank" href="">⬇️ model</a>
<textarea style="display:none"></textarea>
<a-scene>
<a-entity id="model" xrf gltf-model="./../../assets/example3.gltf" />
<a-entity id="floor" gltf-to-entity="from: #model; name: floor" />
<a-entity xrf id="player">
<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-scene light="defaultLightsEnabled: false">
<a-entity xrf id="player" >
<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 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="floor" xrf-get="floor"></a-entity>
</a-scene>
@ -43,16 +42,32 @@
window.$ = (s) => document.querySelector(s)
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', () => {
setupConsole( $('textarea') )
// init navigator url
document.location.hash = $('#uri').value
// update url when sandbox-url is updated
window.addEventListener('hashchange', () => {
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>
</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 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="./main.css">
<link type="text/css" rel="stylesheet" href="./../../assets/style.css"/>
</head>
<body>
@ -96,14 +96,14 @@
scene,
renderer,
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
document.location.hash = $('#uri').value
window.addEventListener('hashchange', () => {
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
XRF.env = (xrf,v,opts) => {
@ -127,35 +127,9 @@
window.XRF = XRF // expose to form
// load 3D asset
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'
let file = document.location.search.length > 2 ? document.location.search.substr(1) : './../../assets/example3.gltf'
$('#model').setAttribute("href","./../../asset/"+file)
loader.load( file, loadGLTF );
XRF.navigate( file )
// setup mouse controls
@ -171,8 +145,13 @@
//controls.maxPolarAngle = Math.PI / 2;
//controls.target = new THREE.Vector3(0,1.6,0)
camera.position.set( 0, 4, 15 );
//controls.update()
//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()
const geometry = new THREE.BufferGeometry();
geometry.setFromPoints( [ new THREE.Vector3( 0, 0, 0 ), new THREE.Vector3( 0, 0, - 5 ) ] );
@ -207,21 +186,12 @@
const gui = new GUI( { width: 300 } );
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 );
mesh.position.x = - 0.75;
mesh.position.y = 1.5;
mesh.position.z = 0.3;
mesh.rotation.y = Math.PI / 4;
mesh.scale.setScalar( 2 );
group.add( mesh );
// Add stats.js
@ -236,7 +206,14 @@
statsMesh.position.z = 0.3;
statsMesh.rotation.y = Math.PI / 4;
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({
".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/xrf/*.js \
src/3rd/aframe/*.js > dist/xrfragment.aframe.js
ls -la dist | grep js
exit $ok
}

View File

@ -4,72 +4,72 @@ window.AFRAME.registerComponent('xrf', {
},
init: function () {
if( !AFRAME.XRF ) this.initXRFragments()
if( !this.rig && this.el.querySelector('[camera]') )
AFRAME.XRF.rig = this.el
if( typeof this.data == "string" ){
AFRAME.XRF.navigate.to(this.data)
.then( (model) => {
let gets = [ ...document.querySelectorAll('[xrf-get]') ]
gets.map( (g) => g.emit('update',model) )
})
}
},
initXRFragments: function(){
let aScene = document.querySelector('a-scene')
// enable XR fragments
let XRF = AFRAME.XRF = xrfragment.init({
THREE,
camera: aScene.camera,
camera: aScene.camera,
scene: aScene.object3D,
renderer: aScene.renderer,
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)
XRF.pos = (xrf,v,opts) => {
let { mesh, model, camera, scene, renderer, THREE} = opts
console.log("!pos")
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 camera-related XR Fragments so the camera-rig is affected
let camOverride = (xrf,v,opts) => {
opts.camera = $('[camera]').object3D //parentElement.object3D
xrf(v,opts)
}
XRF.pos = camOverride
XRF.rot = camOverride
XRF.href = camOverride
// override the 'rot' XR Fragment so we can translate the camera rig (not the camera itself)
XRF.rot = (xrf,v,opts) => {
let { mesh, model, camera, scene, renderer, THREE} = opts
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: {
from: {default: '', type: 'selector'},
name: {default: ''},
duplicate: {type:'boolean'}
name: {type: 'string'},
duplicate: {type: 'boolean'}
},
init: function () {
var el = this.el;
var data = this.data;
data.from.addEventListener('model-loaded', evt => {
var model;
var subset;
model = evt.detail.model;
console.dir(this.data.from)
subset = model.getObjectByName(data.name);
if (!subset){
console.error("Sub-object", data.name, "not found in #"+data.from.id);
var el = this.el;
var meshname = this.data.name || this.data;
this.el.addEventListener('update', (evt) => {
let scene = evt.detail.scene
let mesh = scene.getObjectByName(meshname);
if (!mesh){
console.error("mesh with name '"+meshname+"' not found in model")
return;
}
if( !this.data.duplicate ) subset.parent.remove(subset)
let clone = subset.clone()
////subset.updateMatrixWorld();
el.object3D.position.setFromMatrixPosition(data.from.object3D.matrixWorld);
el.object3D.quaternion.setFromRotationMatrix(data.from.object3D.matrixWorld);
el.setObject3D('mesh', clone );
el.emit('model-loaded', el.getObject3D('mesh'));
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()
////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}`)
})
});
}
});

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 = {}
xrfragment.model = {}
let xrf = xrfragment
xrf.frag = {}
xrf.model = {}
xrfragment.init = function(opts){
xrf.init = function(opts){
opts = opts || {}
let XRF = function(){
alert("queries are not implemented (yet)")
}
for ( let i in opts ) xrfragment[i] = opts[i]
for ( let i in xrfragment.XRF ) xrfragment.XRF[i] // shortcuts to constants (NAVIGATOR e.g.)
xrfragment.Parser.debug = xrfragment.debug
if( opts.loaders ) opts.loaders.map( xrfragment.patchLoader )
xrfragment.patchRenderer(opts.renderer)
return xrfragment
for ( let i in opts ) xrf[i] = opts[i]
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.patchRenderer(opts.renderer)
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){
if( xrfragment.getLastModel() && xrfragment.getLastModel().render )
xrfragment.getLastModel().render(scene,camera)
if( xrf.model && xrf.model.render )
xrf.model.render(scene,camera)
render(scene,camera)
})(renderer.render.bind(renderer))
}
xrfragment.patchLoader = function(loader){
xrf.patchLoader = function(loader){
loader.prototype.load = ((load) => function(url, onLoad, onProgress, onError){
load.call( this,
url,
(model) => { onLoad(model); xrfragment.parseModel(model,url) },
(model) => { onLoad(model); xrf.parseModel(model,url) },
onProgress,
onError)
})(loader.prototype.load)
}
xrfragment.getFile = (url) => url.split("/").pop().replace(/#.*/,'')
xrf.getFile = (url) => url.split("/").pop().replace(/#.*/,'')
xrfragment.parseModel = function(model,url){
let file = xrfragment.getFile(url)
xrf.parseModel = function(model,url){
let file = xrf.getFile(url)
model.file = file
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)
model.scene.traverse( (mesh) => {
console.log("◎ "+mesh.name)
if( mesh.userData ){
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)
}
}
console.log("◎ "+ (mesh.name||`THREE.${mesh.constructor.name}`))
xrf.eval.mesh(mesh,model)
})
}
xrfragment.evalFragment = (k, opts ) => {
// 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.getLastModel = () => xrf.model.last
xrfragment.eval = function( url, model ){
xrf.eval = function( url, model ){
let notice = false
model = model || xrfragment.getLastModel()
let { THREE, camera } = xrfragment
let frag = xrfragment.URI.parse( url, xrfragment.XRF.NAVIGATOR )
model = model || xrf.model
let { THREE, camera } = xrf
let frag = xrf.URI.parse( url, xrf.XRF.NAVIGATOR )
let meshes = frag.q ? [] : [camera]
for ( let i in meshes ) {
for ( let k in frag ){
let mesh = meshes[i]
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 }
xrfragment.evalFragment(k,opts)
let opts = {frag, mesh, model, camera: xrf.camera, scene: xrf.scene, renderer: xrf.renderer, THREE: xrf.THREE }
xrf.eval.fragment(k,opts)
}
}
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 env = mesh.getObjectByName(v.string)
env.material.map.mapping = THREE.EquirectangularReflectionMapping;
scene.environment = env.material.map
scene.texture = env.material.map
//scene.texture = env.material.map
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`)
}

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 size = 5
let texture = mesh.material.map
texture.mapping = THREE.ClampToEdgeWrapping
texture.needsUpdate = true
mesh.material.dispose()
/*
texture.wrapS = texture.wrapT = THREE.ClampToEdgeWrapping;
mesh.material = new THREE.MeshStandardMaterial( {
envMap: texture,
roughness: 0.0,
metalness: 1,
side: THREE.DoubleSide,
})
*/
// poor man's equi-portal
mesh.material = new THREE.ShaderMaterial( {
side: THREE.DoubleSide,
uniforms: {
@ -42,9 +35,58 @@ xrfragment.xrf.href = function(v, opts){
vec3 direction = normalize(vWorldPosition - cameraPosition);
vec2 sampleUV;
sampleUV.y = -clamp(direction.y * 0.5 + 0.5, 0.0, 1.0);
sampleUV.x = atan(direction.z, -direction.x) * -RECIPROCAL_PI2 + 0.5;
gl_FragColor = texture2D(pano, sampleUV);
sampleUV.x = atan(direction.z, -direction.x) * -RECIPROCAL_PI2;
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
console.log(" └ setting camera position to "+v.string)
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
camera.rotation.x = v.x * 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
if( v.string[0] == "#" ){ // local
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
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.y = mesh.position.y
modelInstance.position.x = mesh.position.x
@ -13,18 +16,9 @@ xrfragment.xrf.src = function(v, opts){
modelInstance.scale.y = mesh.scale.y
modelInstance.scale.x = mesh.scale.z
// now apply XR Fragments overrides from URI
// *TODO* move to a central location (pull-up)
for( var i in args ){
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
}
}
for( var i in frag )
xrf.eval.fragment(i, Object.assign(opts,{frag, model:modelInstance,scene:sceneInstance}))
// Add the instance to the scene
scene.add(modelInstance);
console.dir(model)
console.dir(modelInstance)
model.scene.add(modelInstance);
}
}