href work in progress + aframe simplify refactor
This commit is contained in:
parent
21ffc80e07
commit
3cd5a7d333
2
.vimrc
2
.vimrc
|
@ -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>
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
xrf.getLastModel = () => xrf.model.last
|
||||
|
||||
xrfragment.getLastModel = () => Object.values(xrfragment.model)[ Object.values(xrfragment.model).length-1 ]
|
||||
|
||||
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,9 +1044,15 @@ 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
|
||||
|
@ -796,60 +1062,54 @@ window.AFRAME.registerComponent('xrf', {
|
|||
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)
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
}
|
||||
});
|
||||
XRF.pos = camOverride
|
||||
XRF.rot = camOverride
|
||||
XRF.href = camOverride
|
||||
|
||||
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);
|
||||
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}`)
|
||||
|
||||
el.setObject3D('mesh', clone );
|
||||
el.emit('model-loaded', el.getObject3D('mesh'));
|
||||
})
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -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)
|
||||
}
|
||||
xrf.getLastModel = () => xrf.model.last
|
||||
|
||||
xrfragment.getLastModel = () => Object.values(xrfragment.model)[ Object.values(xrfragment.model).length-1 ]
|
||||
|
||||
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;
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
../../assets/example3.gltf
|
|
@ -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>
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
../../assets/other.gltf
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -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,7 +145,12 @@
|
|||
//controls.maxPolarAngle = Math.PI / 2;
|
||||
//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()
|
||||
|
||||
const geometry = new THREE.BufferGeometry();
|
||||
|
@ -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 ) ),
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
../../assets/other.gltf
|
1
make
1
make
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -4,9 +4,15 @@ 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
|
||||
|
@ -16,60 +22,54 @@ window.AFRAME.registerComponent('xrf', {
|
|||
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)
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
}
|
||||
});
|
||||
XRF.pos = camOverride
|
||||
XRF.rot = camOverride
|
||||
XRF.href = camOverride
|
||||
|
||||
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);
|
||||
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}`)
|
||||
|
||||
el.setObject3D('mesh', clone );
|
||||
el.emit('model-loaded', el.getObject3D('mesh'));
|
||||
})
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
xrf.getLastModel = () => xrf.model.last
|
||||
|
||||
xrfragment.getLastModel = () => Object.values(xrfragment.model)[ Object.values(xrfragment.model).length-1 ]
|
||||
|
||||
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}` )
|
||||
}
|
||||
|
|
|
@ -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`)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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}`)
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue