Merge pull request #2 from coderofsalvation/dev
v1 href-fragment + docs
This commit is contained in:
commit
0b1fafc884
57 changed files with 10925 additions and 859 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -1,2 +1,3 @@
|
|||
dist/*.pyc
|
||||
src/spec/tmp.json
|
||||
tags
|
||||
|
|
|
|||
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>
|
||||
|
|
|
|||
18
README.md
18
README.md
|
|
@ -3,19 +3,25 @@
|
|||
|
||||
[](https://github.com/coderofsalvation/xrfragment/actions)
|
||||
|
||||
# usage
|
||||
# Documentation
|
||||
|
||||
https://coderofsalvation.github.io/xrfragment
|
||||
|
||||
# available implementations
|
||||
|
||||
* [lua (handwritten)](dist/xrfragment.lua) [(+example)](src/xrfragment/Parser.lua)
|
||||
* [haXe](src/xrfragment)
|
||||
* [javascript](dist/xrfragment.js) [(+example)](test/test.js)
|
||||
* [python](dist/xrfragment.py) [(+example)](test/test.py)
|
||||
* [lua](dist/xrfragment.lua) [(+example)](test/test.lua)
|
||||
* [javascript](dist/xrfragment.js) [(+example)](test/test.js)
|
||||
* [python](dist/xrfragment.py) [(+example)](test/test.py)
|
||||
* [lua](dist/xrfragment.lua) [(+example)](test/test.lua)
|
||||
|
||||
See documentation for more info
|
||||
|
||||
# development
|
||||
|
||||
Pre-build libraries can be found in [/dist folder](dist)<br>
|
||||
If you really want to build from source:
|
||||
|
||||
```
|
||||
$ ./make install
|
||||
$ ./make && ./make runtest
|
||||
$ ./make build && ./make runtest
|
||||
```
|
||||
|
|
|
|||
3
dist/license.js
vendored
Normal file
3
dist/license.js
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
// SPDX-License-Identifier: MPL-2.0
|
||||
// Copyright (c) 2023 Leon van Kammen/NLNET
|
||||
|
||||
1431
dist/xrfragment.aframe.js
vendored
Normal file
1431
dist/xrfragment.aframe.js
vendored
Normal file
File diff suppressed because it is too large
Load diff
3
dist/xrfragment.module.js
vendored
3
dist/xrfragment.module.js
vendored
|
|
@ -1,3 +1,6 @@
|
|||
// SPDX-License-Identifier: MPL-2.0
|
||||
// Copyright (c) 2023 Leon van Kammen/NLNET
|
||||
|
||||
var $hx_exports = typeof exports != "undefined" ? exports : typeof window != "undefined" ? window : typeof self != "undefined" ? self : this;
|
||||
(function ($global) { "use strict";
|
||||
$hx_exports["xrfragment"] = $hx_exports["xrfragment"] || {};
|
||||
|
|
|
|||
677
dist/xrfragment.three.js
vendored
677
dist/xrfragment.three.js
vendored
|
|
@ -1,3 +1,6 @@
|
|||
// SPDX-License-Identifier: MPL-2.0
|
||||
// Copyright (c) 2023 Leon van Kammen/NLNET
|
||||
|
||||
var $hx_exports = typeof exports != "undefined" ? exports : typeof window != "undefined" ? window : typeof self != "undefined" ? self : this;
|
||||
(function ($global) { "use strict";
|
||||
$hx_exports["xrfragment"] = $hx_exports["xrfragment"] || {};
|
||||
|
|
@ -593,183 +596,605 @@ 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
|
||||
|
||||
// extract camera when camera-rig is passed
|
||||
camera.traverse( (n) => String(n.type).match(/Camera/) ? camera = n : null )
|
||||
|
||||
const scope = this;
|
||||
|
||||
const raycaster = new Raycaster();
|
||||
const tempMatrix = new Matrix4();
|
||||
|
||||
function nocollide(){
|
||||
if( nocollide.tid ) return // ratelimit
|
||||
_event.type = "nocollide"
|
||||
scope.children.map( (c) => c.dispatchEvent(_event) )
|
||||
nocollide.tid = setTimeout( () => nocollide.tid = null, 100 )
|
||||
}
|
||||
|
||||
// Pointer Events
|
||||
|
||||
const element = renderer.domElement;
|
||||
|
||||
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 );
|
||||
|
||||
}else nocollide()
|
||||
|
||||
}
|
||||
|
||||
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 );
|
||||
|
||||
}else nocollide()
|
||||
|
||||
}
|
||||
|
||||
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 ) XRF[i] = xrfragment[i] = opts[i]
|
||||
for ( let i in xrfragment ) XRF[i] = xrfragment[i]
|
||||
for ( let i in xrfragment.XRF ) XRF[i] = xrfragment.XRF[i] // shortcuts to constants (NAVIGATOR e.g.)
|
||||
xrfragment.Parser.debug = xrfragment.debug
|
||||
if( opts.loaders ) opts.loaders.map( xrfragment.patchLoader )
|
||||
return XRF
|
||||
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.navigator.init()
|
||||
return xrf
|
||||
}
|
||||
|
||||
xrfragment.patchLoader = function(loader){
|
||||
xrf.patchRenderer = function(renderer){
|
||||
renderer.xr.addEventListener( 'sessionstart', () => xrf.baseReferenceSpace = renderer.xr.getReferenceSpace() );
|
||||
renderer.xr.enabled = true;
|
||||
renderer.render = ((render) => function(scene,camera){
|
||||
if( xrf.model && xrf.model.render )
|
||||
xrf.model.render(scene,camera)
|
||||
render(scene,camera)
|
||||
})(renderer.render.bind(renderer))
|
||||
}
|
||||
|
||||
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
|
||||
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 )
|
||||
// call native function (xrf/env.js e.g.), or pass it to user decorator
|
||||
let func = xrfragment.xrf[k] || function(){}
|
||||
let opts = {mesh, model, camera: xrfragment.camera, scene: xrfragment.scene, renderer: xrfragment.renderer, THREE: xrfragment.THREE }
|
||||
if( xrfragment[k] ) xrfragment[k]( func, frag[k], opts)
|
||||
else func( frag[k], opts)
|
||||
}
|
||||
}
|
||||
})
|
||||
// eval embedded XR fragments
|
||||
model.scene.traverse( (mesh) => xrf.eval.mesh(mesh,model) )
|
||||
}
|
||||
|
||||
xrfragment.getLastModel = () => Object.values(xrfragment.model)[ Object.values(xrfragment.model).length-1 ]
|
||||
|
||||
xrfragment.eval = function( url, model ){
|
||||
xrf.getLastModel = () => xrf.model.last
|
||||
|
||||
xrf.eval = function( url, model ){
|
||||
let notice = false
|
||||
let { THREE, camera } = xrfragment
|
||||
let frag = xrfragment.URI.parse( url, 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 frag ) {
|
||||
if( !String(i).match(/(pos|rot)/) ) notice = true
|
||||
if( i == "pos" ){
|
||||
camera.position.x = frag.pos.x
|
||||
camera.position.y = frag.pos.y
|
||||
camera.position.z = frag.pos.z
|
||||
}
|
||||
if( i == "rot" ){
|
||||
camera.rotation.x = THREE.MathUtils.degToRad( frag.pos.x )
|
||||
camera.rotation.y = THREE.MathUtils.degToRad( frag.pos.y )
|
||||
camera.rotation.z = THREE.MathUtils.degToRad( frag.pos.z )
|
||||
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: 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)")
|
||||
console.dir({url,model,frag})
|
||||
}
|
||||
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 }
|
||||
mesh.userData.XRF = frag // allow fragment impl to access XRF obj already
|
||||
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 = () => {
|
||||
const disposeObject = (obj) => {
|
||||
if (obj.children.length > 0) obj.children.forEach((child) => disposeObject(child));
|
||||
if (obj.geometry) obj.geometry.dispose();
|
||||
if (obj.material) {
|
||||
if (obj.material.map) obj.material.map.dispose();
|
||||
obj.material.dispose();
|
||||
}
|
||||
obj.clear()
|
||||
obj.removeFromParent()
|
||||
return true
|
||||
};
|
||||
let nodes = []
|
||||
xrf.scene.traverse( (child) => child.isXRF ? nodes.push(child) : false )
|
||||
nodes.map( disposeObject ) // leave non-XRF objects intact
|
||||
xrf.interactive = xrf.InteractiveGroup( xrf.THREE, xrf.renderer, xrf.camera)
|
||||
xrf.add( xrf.interactive)
|
||||
}
|
||||
|
||||
xrf.parseUrl = (url) => {
|
||||
const urlObj = new URL( url.match(/:\/\//) ? url : String(`https://fake.com/${url}`).replace(/\/\//,'/') )
|
||||
let dir = url.substring(0, url.lastIndexOf('/') + 1)
|
||||
const file = urlObj.pathname.substring(urlObj.pathname.lastIndexOf('/') + 1);
|
||||
const hash = url.match(/#/) ? url.replace(/.*#/,'') : ''
|
||||
const ext = file.split('.').pop()
|
||||
return {urlObj,dir,file,hash,ext}
|
||||
}
|
||||
|
||||
xrf.add = (object) => {
|
||||
object.isXRF = true // mark for easy deletion when replacing scene
|
||||
xrf.scene.add(object)
|
||||
}
|
||||
|
||||
/*
|
||||
* EVENTS
|
||||
*/
|
||||
|
||||
xrf.addEventListener = function(eventName, callback) {
|
||||
if( !this._listeners ) this._listeners = []
|
||||
if (!this._listeners[eventName]) {
|
||||
// create a new array for this event name if it doesn't exist yet
|
||||
this._listeners[eventName] = [];
|
||||
}
|
||||
// add the callback to the listeners array for this event name
|
||||
this._listeners[eventName].push(callback);
|
||||
};
|
||||
|
||||
xrf.emit = function(eventName, data) {
|
||||
if( !this._listeners ) this._listeners = []
|
||||
var callbacks = this._listeners[eventName]
|
||||
if (callbacks) {
|
||||
for (var i = 0; i < callbacks.length; i++) {
|
||||
callbacks[i](data);
|
||||
}
|
||||
}
|
||||
};
|
||||
xrf.navigator = {}
|
||||
|
||||
xrf.navigator.to = (url,event) => {
|
||||
if( !url ) throw 'xrf.navigator.to(..) no url given'
|
||||
return new Promise( (resolve,reject) => {
|
||||
let {urlObj,dir,file,hash,ext} = xrf.parseUrl(url)
|
||||
console.log("xrfragment: navigating to "+url)
|
||||
|
||||
if( !file || xrf.model.file == file ){ // we're already loaded
|
||||
document.location.hash = `#${hash}` // just update the hash
|
||||
xrf.eval( url, xrf.model ) // and eval URI XR fragments
|
||||
return resolve(xrf.model)
|
||||
}
|
||||
|
||||
if( xrf.model && xrf.model.scene ) xrf.model.scene.visible = false
|
||||
const Loader = xrf.loaders[ext]
|
||||
if( !Loader ) throw 'xrfragment: no loader passed to xrfragment for extension .'+ext
|
||||
xrf.reset() // clear xrf objects from scene
|
||||
// force relative path
|
||||
if( dir ) dir = dir[0] == '.' ? dir : `.${dir}`
|
||||
const loader = new Loader().setPath( dir )
|
||||
loader.load( file, function(model){
|
||||
model.file = file
|
||||
xrf.add( model.scene )
|
||||
xrf.model = model
|
||||
xrf.eval( url, model ) // and eval URI XR fragments
|
||||
xrf.navigator.pushState( file, hash )
|
||||
resolve(model)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
xrf.navigator.init = () => {
|
||||
if( xrf.navigator.init.inited ) return
|
||||
window.addEventListener('popstate', function (event){
|
||||
xrf.navigator.to( document.location.search.substr(1) + document.location.hash, event)
|
||||
})
|
||||
xrf.navigator.init.inited = true
|
||||
}
|
||||
|
||||
xrf.navigator.pushState = (file,hash) => {
|
||||
if( file == document.location.search.substr(1) ) return // page is in its default state
|
||||
window.history.pushState({},`${file}#${hash}`, document.location.pathname + `?${file}#${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;
|
||||
console.log(` └ applied image '${v.string}' as environtment map`)
|
||||
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){
|
||||
/**
|
||||
* navigation, portals & mutations
|
||||
*
|
||||
* | fragment | type | scope | example value |
|
||||
* |`href`| string (uri or [predefined view](#predefined_view )) | 🔒 |`#pos=1,1,0`<br>`#pos=1,1,0&rot=90,0,0`<br>`#pos=pyramid`<br>`#pos=lastvisit\|pyramid`<br>`://somefile.gltf#pos=1,1,0`<br> |
|
||||
*
|
||||
* [img[xrfragment.jpg]]
|
||||
*
|
||||
* !!!spec 1.0
|
||||
*
|
||||
* 1. a ''external''- or ''file URI'' fully replaces the current scene and assumes `pos=0,0,0&rot=0,0,0` by default (unless specified)
|
||||
*
|
||||
* 2. navigation should not happen when queries (`q=`) are present in local url: queries will apply (`pos=`, `rot=` e.g.) to the targeted object(s) instead.
|
||||
*
|
||||
* 3. navigation should not happen ''immediately'' when user is more than 2 meter away from the portal/object containing the href (to prevent accidental navigation e.g.)
|
||||
*/
|
||||
|
||||
xrf.frag.href = function(v, opts){
|
||||
let { mesh, model, camera, scene, renderer, THREE} = opts
|
||||
return
|
||||
|
||||
// Create a shader material that treats the texture as an equirectangular map
|
||||
mesh.texture = mesh.material.map // backup texture
|
||||
const equirectShader = THREE.ShaderLib[ 'equirect' ];
|
||||
const equirectMaterial = new THREE.ShaderMaterial( {
|
||||
uniforms: THREE.UniformsUtils.merge([
|
||||
THREE.UniformsLib.equirect,
|
||||
equirectShader.uniforms,
|
||||
]),
|
||||
vertexShader: equirectShader.vertexShader,
|
||||
fragmentShader: equirectShader.fragmentShader,
|
||||
side: THREE.DoubleSide //THREE.FrontSide //THREE.DoubleSide //THREE.BackSide
|
||||
} );
|
||||
equirectMaterial.uniforms[ 'tEquirect' ].value = mesh.texture
|
||||
// Define the tEquirectInvProjection uniform
|
||||
equirectMaterial.uniforms.tEquirectInvProjection = {
|
||||
value: new THREE.Matrix4(),
|
||||
};
|
||||
// Assign the new material to the mesh
|
||||
mesh.material = equirectMaterial;
|
||||
console.dir(mesh.material)
|
||||
mesh.texture.wrapS = THREE.RepeatWrapping;
|
||||
const world = {
|
||||
pos: new THREE.Vector3(),
|
||||
scale: new THREE.Vector3(),
|
||||
quat: new THREE.Quaternion()
|
||||
}
|
||||
mesh.getWorldPosition(world.pos)
|
||||
mesh.getWorldScale(world.scale)
|
||||
mesh.getWorldQuaternion(world.quat);
|
||||
mesh.position.copy(world.pos)
|
||||
mesh.scale.copy(world.scale)
|
||||
mesh.setRotationFromQuaternion(world.quat);
|
||||
|
||||
// patch custom model renderloop
|
||||
model.render = ((render) => (scene,camera) => {
|
||||
// detect equirectangular image
|
||||
let texture = mesh.material.map
|
||||
if( texture && texture.source.data.height == texture.source.data.width/2 ){
|
||||
texture.mapping = THREE.ClampToEdgeWrapping
|
||||
texture.needsUpdate = true
|
||||
|
||||
// Store the original projection matrix of the camera
|
||||
const originalProjectionMatrix = camera.projectionMatrix.clone();
|
||||
// Calculate the current camera view matrix
|
||||
const aspectRatio = mesh.texture.image.width / mesh.texture.image.height;
|
||||
camera.projectionMatrix.makePerspective(camera.fov, aspectRatio, camera.near, camera.far);
|
||||
// poor man's equi-portal
|
||||
mesh.material = new THREE.ShaderMaterial( {
|
||||
side: THREE.DoubleSide,
|
||||
uniforms: {
|
||||
pano: { value: texture },
|
||||
selected: { value: false },
|
||||
},
|
||||
vertexShader: `
|
||||
vec3 portalPosition;
|
||||
varying vec3 vWorldPosition;
|
||||
varying float vDistanceToCenter;
|
||||
varying float vDistance;
|
||||
void main() {
|
||||
vDistanceToCenter = clamp(length(position - vec3(0.0, 0.0, 0.0)), 0.0, 1.0);
|
||||
portalPosition = (modelMatrix * vec4(0.0, 0.0, 0.0, 1.0)).xyz;
|
||||
vDistance = length(portalPosition - cameraPosition);
|
||||
vWorldPosition = (modelMatrix * vec4(position, 1.0)).xyz;
|
||||
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
|
||||
}
|
||||
`,
|
||||
fragmentShader: `
|
||||
#define RECIPROCAL_PI2 0.15915494
|
||||
uniform sampler2D pano;
|
||||
uniform bool selected;
|
||||
varying float vDistanceToCenter;
|
||||
varying float vDistance;
|
||||
varying vec3 vWorldPosition;
|
||||
void main() {
|
||||
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;
|
||||
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 = selected ? color : vec4(vec3(luminance) + vec3(0.33), color.a);
|
||||
gl_FragColor = grayscale_color;
|
||||
}
|
||||
`,
|
||||
});
|
||||
mesh.material.needsUpdate = true
|
||||
}
|
||||
|
||||
const viewMatrix = camera.matrixWorldInverse;
|
||||
const worldMatrix = mesh.matrixWorld;
|
||||
let teleport = mesh.userData.XRF.href.exec = (e) => {
|
||||
const meshWorldPosition = new THREE.Vector3();
|
||||
meshWorldPosition.setFromMatrixPosition(mesh.matrixWorld);
|
||||
|
||||
const equirectInvProjection = new THREE.Matrix4();
|
||||
equirectInvProjection.copy(camera.projectionMatrix).multiply(viewMatrix).invert();
|
||||
let portalArea = 1 // 2 meter
|
||||
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);
|
||||
|
||||
// Update the equirectangular material's tEquirect uniform
|
||||
equirectMaterial.uniforms.tEquirect.value = mesh.texture;
|
||||
equirectMaterial.uniforms.tEquirectInvProjection.value.copy(
|
||||
equirectInvProjection
|
||||
);
|
||||
const distance = camera.position.distanceTo(newPos);
|
||||
//if( distance > portalArea ){
|
||||
if( !renderer.xr.isPresenting && !confirm("teleport to "+v.string+" ?") ) return
|
||||
|
||||
xrf.navigator.to(v.string) // ok let's surf to HREF!
|
||||
console.log("teleport!")
|
||||
|
||||
xrf.emit('href',{click:true,mesh,xrf:v})
|
||||
}
|
||||
|
||||
// Reset the camera projection matrix
|
||||
camera.projectionMatrix.copy(originalProjectionMatrix);
|
||||
let selected = (state) => () => {
|
||||
if( mesh.selected == state ) return // nothing changed
|
||||
if( mesh.material.uniforms ) mesh.material.uniforms.selected.value = state
|
||||
else mesh.material.color.r = mesh.material.color.g = mesh.material.color.b = state ? 2.0 : 1.0
|
||||
// update mouse cursor
|
||||
if( !renderer.domElement.lastCursor )
|
||||
renderer.domElement.lastCursor = renderer.domElement.style.cursor
|
||||
renderer.domElement.style.cursor = state ? 'pointer' : renderer.domElement.lastCursor
|
||||
xrf.emit('href',{selected:state,mesh,xrf:v})
|
||||
mesh.selected = state
|
||||
}
|
||||
|
||||
if( !opts.frag.q ){ // query means an action
|
||||
mesh.addEventListener('click', teleport )
|
||||
mesh.addEventListener('mousemove', selected(true) )
|
||||
mesh.addEventListener('nocollide', selected(false) )
|
||||
}
|
||||
|
||||
render(scene,camera)
|
||||
|
||||
})(model.render)
|
||||
|
||||
console.dir(mesh)
|
||||
// lazy add mesh (because we're inside a recursive traverse)
|
||||
setTimeout( (mesh) => {
|
||||
xrf.interactive.add(mesh)
|
||||
}, 20, mesh )
|
||||
}
|
||||
xrfragment.xrf.pos = function(v, opts){
|
||||
let { mesh, model, camera, scene, renderer, THREE} = opts
|
||||
|
||||
/**
|
||||
* > above was abducted from [[this|https://i.imgur.com/E3En0gJ.png]] and [[this|https://i.imgur.com/lpnTz3A.png]] survey result
|
||||
*
|
||||
* [[» discussion|https://github.com/coderofsalvation/xrfragment/issues/1]]<br>
|
||||
* [[» implementation example|https://github.com/coderofsalvation/xrfragment/blob/main/src/three/xrf/pos.js]]<br>
|
||||
*/
|
||||
xrf.frag.pos = function(v, opts){
|
||||
//if( renderer.xr.isPresenting ) return // too far away
|
||||
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
|
||||
|
||||
if( !frag.q ){
|
||||
|
||||
if( true ){//!renderer.xr.isPresenting ){
|
||||
console.dir(camera)
|
||||
camera.position.x = v.x
|
||||
camera.position.y = v.y
|
||||
camera.position.z = v.z
|
||||
}
|
||||
/*
|
||||
else{ // XR
|
||||
let cameraWorldPosition = new THREE.Vector3()
|
||||
camera.object3D.getWorldPosition(this.cameraWorldPosition)
|
||||
let newRigWorldPosition = new THREE.Vector3(v.x,v.y,x.z)
|
||||
|
||||
// Finally update the cameras position
|
||||
let newRigLocalPosition.copy(this.newRigWorldPosition)
|
||||
if (camera.object3D.parent) {
|
||||
camera.object3D.parent.worldToLocal(newRigLocalPosition)
|
||||
}
|
||||
camera.setAttribute('position', newRigLocalPosition)
|
||||
|
||||
// Also take the headset/camera rotation itself into account
|
||||
if (this.data.rotateOnTeleport) {
|
||||
this.teleportOcamerainQuaternion
|
||||
.setFromEuler(new THREE.Euler(0, this.teleportOcamerain.object3D.rotation.y, 0))
|
||||
this.teleportOcamerainQuaternion.invert()
|
||||
this.teleportOcamerainQuaternion.multiply(this.hitEntityQuaternion)
|
||||
// Rotate the camera based on calculated teleport ocamerain rotation
|
||||
this.cameraRig.object3D.setRotationFromQuaternion(this.teleportOcamerainQuaternion)
|
||||
}
|
||||
|
||||
console.log("XR")
|
||||
const offsetPosition = { x: - v.x, y: - v.y, z: - v.z, w: 1 };
|
||||
const offsetRotation = new THREE.Quaternion();
|
||||
const transform = new XRRigidTransform( offsetPosition, offsetRotation );
|
||||
const teleportSpaceOffset = xrf.baseReferenceSpace.getOffsetReferenceSpace( transform );
|
||||
renderer.xr.setReferenceSpace( teleportSpaceOffset );
|
||||
}
|
||||
*/
|
||||
|
||||
}
|
||||
}
|
||||
xrfragment.xrf.src = 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
|
||||
console.log(" └ setting camera rotation to "+v.string)
|
||||
camera.rotation.set(
|
||||
v.x * Math.PI / 180,
|
||||
v.y * Math.PI / 180,
|
||||
v.z * Math.PI / 180
|
||||
)
|
||||
}
|
||||
// *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());
|
||||
modelInstance.position.z = mesh.position.x
|
||||
modelInstance.position.y = mesh.position.y
|
||||
modelInstance.position.x = mesh.position.z
|
||||
modelInstance.scale.z = mesh.scale.x
|
||||
modelInstance.scale.y = mesh.scale.y
|
||||
modelInstance.scale.x = mesh.scale.z
|
||||
// now apply XR Fragments overrides from URI
|
||||
// *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
|
||||
}
|
||||
let sceneInstance = new THREE.Group()
|
||||
sceneInstance.isSrc = true
|
||||
|
||||
// prevent infinite recursion #1: skip src-instanced models
|
||||
for ( let i in model.scene.children ) {
|
||||
let child = model.scene.children[i]
|
||||
if( child.isSrc ) continue;
|
||||
sceneInstance.add( model.scene.children[i].clone() )
|
||||
}
|
||||
// Add the instance to the scene
|
||||
scene.add(modelInstance);
|
||||
console.dir(model)
|
||||
console.dir(modelInstance)
|
||||
|
||||
sceneInstance.position.copy( mesh.position )
|
||||
sceneInstance.scale.copy(mesh.scale)
|
||||
sceneInstance.updateMatrixWorld(true) // needed because we're going to move portals to the interactive-group
|
||||
|
||||
// apply embedded XR fragments
|
||||
setTimeout( () => {
|
||||
sceneInstance.traverse( (m) => {
|
||||
if( m.userData && m.userData.src ) return ;//delete m.userData.src // prevent infinite recursion
|
||||
xrf.eval.mesh(m,{scene,recursive:true})
|
||||
})
|
||||
// apply URI XR Fragments inside src-value
|
||||
for( var i in frag ){
|
||||
xrf.eval.fragment(i, Object.assign(opts,{frag, model:{scene:sceneInstance},scene:sceneInstance}))
|
||||
}
|
||||
// Add the instance to the scene
|
||||
model.scene.add(sceneInstance);
|
||||
},10)
|
||||
}
|
||||
}
|
||||
export default xrfragment;
|
||||
|
|
|
|||
1
example/aframe/sandbox/assets
Symbolic link
1
example/aframe/sandbox/assets
Symbolic link
|
|
@ -0,0 +1 @@
|
|||
../../assets
|
||||
1
example/aframe/sandbox/example3.gltf
Symbolic link
1
example/aframe/sandbox/example3.gltf
Symbolic link
|
|
@ -0,0 +1 @@
|
|||
../../assets/example3.gltf
|
||||
1
example/aframe/sandbox/href.gltf
Symbolic link
1
example/aframe/sandbox/href.gltf
Symbolic link
|
|
@ -0,0 +1 @@
|
|||
../../assets/href.gltf
|
||||
67
example/aframe/sandbox/index.html
Normal file
67
example/aframe/sandbox/index.html
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>AFRAME - xrfragment sandbox</title>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
|
||||
<link rel="stylesheet" href="./../../assets/css/axist.min.css" />
|
||||
<link type="text/css" rel="stylesheet" href="./../../assets/css/style.css"/>
|
||||
<script async src="./../../assets/js/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>
|
||||
<body>
|
||||
<div id="overlay">
|
||||
<img src="./../../assets/logo.png" class="logo"/>
|
||||
<button id="navback" onclick="history.back()"><</button>
|
||||
<button id="navforward" onclick="history.forward()">></button>
|
||||
<input type="submit" value="load 3D asset"></input>
|
||||
<input type="text" id="uri" value="" onchange="AFRAME.XRF.navigator.to( $('#uri').value )"/>
|
||||
</div>
|
||||
<a class="btn-foot" id="source" target="_blank" href="https://github.com/coderofsalvation/xrfragment/blob/main/example/aframe/sandbox/index.html">sourcecode</a>
|
||||
<a class="btn-foot" id="model" target="_blank" href="">⬇️ model</a>
|
||||
<textarea style="display:none"></textarea>
|
||||
<a-scene light="defaultLightsEnabled: false">
|
||||
<a-entity id="player" wasd-controls look-controls>
|
||||
<a-entity id="left-hand" laser-controls="hand: left" raycaster="objects:.ray;far:5500" oculus-touch-controls="hand: left" blink-controls="cameraRig:#player; teleportOrigin: #camera; collisionEntities: #floor">
|
||||
<a-entity rotation="-90 0 0" position="0 0.1 0">
|
||||
<a-entity id="back" xrf-button="label: <; width:0.05; action: history.back()" position="-0.025 0 0" class="ray"></a-entity>
|
||||
<a-entity id="next" xrf-button="label: >; width:0.05; action: history.forward()" position=" 0.025 0 0" class="ray"></a-entity>
|
||||
</a-entity>
|
||||
</a-entity>
|
||||
<a-entity id="right-hand" laser-controls="hand: right" raycaster="objects:.ray;far:5500" oculus-touch-controls="hand: right" blink-controls="cameraRig:#player; teleportOrigin: #camera; collisionEntities: #floor"></a-entity>
|
||||
<a-entity camera="fov:90" position="0 1.6 0" id="camera"></a-entity>
|
||||
</a-entity>
|
||||
|
||||
<a-entity id="home" xrf="example3.gltf#pos=0,0,0"></a-entity>
|
||||
<a-entity id="floor" xrf-get="floor"></a-entity>
|
||||
</a-scene>
|
||||
|
||||
<script type="module">
|
||||
import { loadFile, setupConsole, setupUrlBar, notify } from './../../assets/js/utils.js';
|
||||
window.$ = (s) => document.querySelector(s)
|
||||
window.notify = notify(window)
|
||||
console.log = ( (log) => function(str){
|
||||
if( String(str).match(/(camera)/) ) window.notify(str)
|
||||
log(str)
|
||||
})(console.log)
|
||||
|
||||
if( document.location.search.length > 2 )
|
||||
$('#home').setAttribute('xrf', document.location.search.substr(1)+document.location.hash )
|
||||
|
||||
$('a-scene').addEventListener('loaded', () => {
|
||||
setupConsole( $('textarea') )
|
||||
setupUrlBar( $('input#uri') )
|
||||
|
||||
// add screenshot component with camera to capture proper equirects
|
||||
$('a-scene').setAttribute("screenshot",{camera: "[camera]",width: 4096*2, height:2048*2})
|
||||
|
||||
setTimeout( () => window.notify("use WASD-keys and mouse-drag to move around",{timeout:false}),2000 )
|
||||
|
||||
window.AFRAME.XRF.addEventListener('href', (data) => data.selected ? window.notify(`href: ${data.xrf.string}`) : false )
|
||||
|
||||
})
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
1
example/aframe/sandbox/other.gltf
Symbolic link
1
example/aframe/sandbox/other.gltf
Symbolic link
|
|
@ -0,0 +1 @@
|
|||
../../assets/other.gltf
|
||||
250
example/assets/css/style.css
Normal file
250
example/assets/css/style.css
Normal file
|
|
@ -0,0 +1,250 @@
|
|||
:root {
|
||||
--primary: #6839dc;
|
||||
--light-primary: #ea23cf;
|
||||
--secondary: #872eff;
|
||||
--light-secondary: #ce7df2;
|
||||
--red: red;
|
||||
--black: #424280;
|
||||
--white: #fdfdfd;
|
||||
--dark-gray: #343334;
|
||||
--gray: #ecf7ff47;
|
||||
--light-gray: #efefef;
|
||||
--lighter-gray: #e4e2fb96;
|
||||
--font-sans-serif: system-ui, -apple-system, segoe ui, roboto, ubuntu, helvetica, cantarell, noto sans, sans-serif;
|
||||
--font-monospace: menlo, monaco, lucida console, liberation mono, dejavu sans mono, bitstream vera sans mono, courier new, monospace, serif;
|
||||
--border-radius: 0.2rem;
|
||||
}
|
||||
|
||||
.small{
|
||||
font-size:12px;
|
||||
}
|
||||
|
||||
textarea, select, input[type="text"] {
|
||||
background: transparent; /* linear-gradient( var(--lighter-gray), var(--gray) ) !important; */
|
||||
}
|
||||
|
||||
input[type="submit"] {
|
||||
color: var(--light-gray);
|
||||
}
|
||||
|
||||
.title {
|
||||
border-bottom: 2px solid var(--secondary);
|
||||
padding-bottom: 20px;
|
||||
}
|
||||
|
||||
#overlay{
|
||||
background: #FFF;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 48px;
|
||||
box-shadow: 0px 0px 10px #0004;
|
||||
opacity: 0.9;
|
||||
z-index:2000;
|
||||
}
|
||||
|
||||
#overlay .logo{
|
||||
width: 92px;
|
||||
position: absolute;
|
||||
top: 7px;
|
||||
left: 93px;
|
||||
}
|
||||
|
||||
#overlay > input[type="submit"] {
|
||||
height: 32px;
|
||||
font-size: 14px;
|
||||
position: absolute;
|
||||
right: 9px;
|
||||
top: 8px;
|
||||
}
|
||||
#overlay > button#navback,
|
||||
#overlay > button#navforward {
|
||||
height: 32px;
|
||||
font-size: 14px;
|
||||
position: absolute;
|
||||
left: 9px;
|
||||
padding: 2px 13px;
|
||||
top: 8px;
|
||||
color: var(--light-gray);
|
||||
}
|
||||
#overlay > button#navforward {
|
||||
left:49px;
|
||||
}
|
||||
|
||||
#overlay > #uri {
|
||||
display:none;
|
||||
height: 29px;
|
||||
font-size: 14px;
|
||||
position: absolute;
|
||||
left: 200px;
|
||||
top: 9px;
|
||||
max-width:550px;
|
||||
padding: 5px 0px 5px 5px;
|
||||
width: calc( 63% - 200px);
|
||||
background: #f0f0f0;
|
||||
border-color: #Ccc;
|
||||
}
|
||||
|
||||
.btn-foot{
|
||||
background: white;
|
||||
border-radius: 10px;
|
||||
border: 5px solid #1c1c3299;
|
||||
padding: 0px 6px;
|
||||
bottom:67px;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
a.btn-foot#source{
|
||||
right: 10px;
|
||||
color: #888;
|
||||
font-weight: bold;
|
||||
font-family: sans-serif;
|
||||
z-index:2000;
|
||||
bottom: 114px;
|
||||
}
|
||||
|
||||
a.btn-foot#model{
|
||||
position: absolute;
|
||||
right: 10px;
|
||||
color: #7c7c7c;
|
||||
font-weight: bold;
|
||||
font-family: sans-serif;
|
||||
z-index:2000;
|
||||
}
|
||||
|
||||
html.a-fullscreen a#model,
|
||||
html.a-fullscreen a#source{
|
||||
margin-right:10px;
|
||||
}
|
||||
|
||||
.render {
|
||||
position:absolute;
|
||||
top:0;
|
||||
left:0;
|
||||
right:0;
|
||||
bottom:0;
|
||||
}
|
||||
|
||||
.lil-gui.autoPlace{
|
||||
right:0px !important;
|
||||
top:48px !important;
|
||||
height:33vh;
|
||||
}
|
||||
|
||||
#VRButton {
|
||||
margin-bottom:20vh;
|
||||
}
|
||||
|
||||
@media (max-width: 450px) {
|
||||
#uri{ display:none; }
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.lil-gui.root{
|
||||
top:auto !important;
|
||||
left:auto !important;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* notifications */
|
||||
.js-snackbar-container {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
left: 0px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width:100%;
|
||||
max-width: 100%;
|
||||
padding: 10px;
|
||||
z-index:1001;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.js-snackbar-container * {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.js-snackbar__wrapper {
|
||||
--color-c: #555;
|
||||
--color-a: #EEE;
|
||||
}
|
||||
|
||||
|
||||
.js-snackbar__wrapper {
|
||||
overflow: hidden;
|
||||
height: auto;
|
||||
margin: 5px 0;
|
||||
transition: all ease .5s;
|
||||
border-radius: 3px;
|
||||
box-shadow: 0 0 4px 0 #0007;
|
||||
left: 20px;
|
||||
position: fixed;
|
||||
bottom: 20px;
|
||||
}
|
||||
|
||||
.js-snackbar {
|
||||
display: inline-flex;
|
||||
box-sizing: border-box;
|
||||
border-radius: 3px;
|
||||
color: var(--color-c);
|
||||
font-size: 16px;
|
||||
background-color: var(--color-a);
|
||||
vertical-align: bottom;
|
||||
}
|
||||
|
||||
.js-snackbar__close,
|
||||
.js-snackbar__status,
|
||||
.js-snackbar__message {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.js-snackbar__message {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.js-snackbar__status {
|
||||
display: none;
|
||||
width: 15px;
|
||||
margin-right: 5px;
|
||||
border-radius: 3px 0 0 3px;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.js-snackbar__status.js-snackbar--success,
|
||||
.js-snackbar__status.js-snackbar--warning,
|
||||
.js-snackbar__status.js-snackbar--danger,
|
||||
.js-snackbar__status.js-snackbar--info {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.js-snackbar__status.js-snackbar--success {
|
||||
background-color: #4caf50;
|
||||
}
|
||||
|
||||
.js-snackbar__status.js-snackbar--warning {
|
||||
background-color: #ff9800;
|
||||
}
|
||||
|
||||
.js-snackbar__status.js-snackbar--danger {
|
||||
background-color: #ff6060;
|
||||
}
|
||||
|
||||
.js-snackbar__status.js-snackbar--info {
|
||||
background-color: #CCC;
|
||||
}
|
||||
|
||||
.js-snackbar__close {
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 10px;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.js-snackbar__close:hover {
|
||||
background-color: #4443;
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
2209
example/assets/example3.gltf
Normal file
2209
example/assets/example3.gltf
Normal file
File diff suppressed because one or more lines are too long
2209
example/assets/href.gltf
Normal file
2209
example/assets/href.gltf
Normal file
File diff suppressed because one or more lines are too long
267
example/assets/js/utils.js
Normal file
267
example/assets/js/utils.js
Normal file
|
|
@ -0,0 +1,267 @@
|
|||
|
||||
// contentLoaders = {".gltf" : () => .....} and so on
|
||||
|
||||
export function loadFile(contentLoaders, multiple){
|
||||
return () => {
|
||||
let input = document.createElement('input');
|
||||
input.type = 'file';
|
||||
input.multiple = multiple;
|
||||
input.accept = Object.keys(contentLoaders).join(",");
|
||||
input.onchange = () => {
|
||||
let files = Array.from(input.files);
|
||||
let file = files.slice ? files[0] : files
|
||||
for( var i in contentLoaders ){
|
||||
let r = new RegExp('\\'+i+'$')
|
||||
if( file.name.match(r) ) return contentLoaders[i](file)
|
||||
}
|
||||
alert(file.name+" is not supported")
|
||||
};
|
||||
input.click();
|
||||
}
|
||||
}
|
||||
|
||||
export function setupConsole(el){
|
||||
if( !el ) return setTimeout( () => setupConsole( $('.lil-gui') ),200 )
|
||||
let $console = document.createElement('textarea')
|
||||
$console.style.position = 'absolute'
|
||||
$console.style.display = 'block'
|
||||
$console.style.zIndex = 2000;
|
||||
$console.style.background = "transparent !important"
|
||||
$console.style.pointerEvents = 'none'
|
||||
$console.style.top = '70px'
|
||||
$console.style.padding = '10px'
|
||||
$console.style.margin = '10px'
|
||||
$console.style.background = '#000'
|
||||
$console.style.left = $console.style.right = $console.style.bottom = 0;
|
||||
$console.style.color = '#A6F';
|
||||
$console.style.fontSize = '10px';
|
||||
$console.style.fontFamily = 'Courier'
|
||||
$console.style.border = '0'
|
||||
$console.innerHTML = "XRFRAGMENT CONSOLE OUTPUT:\n"
|
||||
|
||||
el.appendChild($console)
|
||||
|
||||
console.log = ( (log) => function(){
|
||||
let str = ([...arguments]).join(" ")
|
||||
let s = new Date().toISOString().substr(11).substr(0,8) + " " + str.replace(/.*[0-9]: /,"")
|
||||
log(s)
|
||||
let lines = String($console.innerHTML + "\n"+s).split("\n")
|
||||
while( lines.length > 200 ) lines.shift()
|
||||
$console.innerHTML = lines.join("\n")
|
||||
$console.scrollTop = $console.scrollHeight;
|
||||
})(console.log.bind(console))
|
||||
}
|
||||
|
||||
export function setupUrlBar(el){
|
||||
|
||||
var isIframe = (window === window.parent || window.opener) ? false : true;
|
||||
if( isIframe ){
|
||||
// show internal URL bar to test XR fragments interactively
|
||||
el.style.display = 'block'
|
||||
let nav = window.AFRAME.XRF.navigator
|
||||
|
||||
AFRAME.XRF.navigator.to = ((to) => (url,e) => {
|
||||
to(url,e)
|
||||
reflectUrl(url)
|
||||
})(AFRAME.XRF.navigator.to)
|
||||
|
||||
const reflectUrl = (url) => el.value = url || document.location.search.substr(1) + document.location.hash
|
||||
reflectUrl()
|
||||
}
|
||||
}
|
||||
|
||||
function SnackBar(userOptions) {
|
||||
var snackbar = this || (window.snackbar = {});
|
||||
var _Interval;
|
||||
var _Message;
|
||||
var _Element;
|
||||
var _Container;
|
||||
|
||||
var _OptionDefaults = {
|
||||
message: "Operation performed successfully.",
|
||||
dismissible: true,
|
||||
timeout: 5000,
|
||||
status: ""
|
||||
}
|
||||
var _Options = _OptionDefaults;
|
||||
|
||||
function _Create() {
|
||||
_Container = document.getElementsByClassName("js-snackbar-container")[0];
|
||||
|
||||
if (!_Container) {
|
||||
// need to create a new container for notifications
|
||||
_Container = document.createElement("div");
|
||||
_Container.classList.add("js-snackbar-container");
|
||||
|
||||
document.body.appendChild(_Container);
|
||||
}
|
||||
_Element = document.createElement("div");
|
||||
_Element.classList.add("js-snackbar__wrapper");
|
||||
|
||||
let innerSnack = document.createElement("div");
|
||||
innerSnack.classList.add("js-snackbar", "js-snackbar--show");
|
||||
|
||||
if (_Options.status) {
|
||||
_Options.status = _Options.status.toLowerCase().trim();
|
||||
|
||||
let status = document.createElement("span");
|
||||
status.classList.add("js-snackbar__status");
|
||||
|
||||
|
||||
if (_Options.status === "success" || _Options.status === "green") {
|
||||
status.classList.add("js-snackbar--success");
|
||||
}
|
||||
else if (_Options.status === "warning" || _Options.status === "alert" || _Options.status === "orange") {
|
||||
status.classList.add("js-snackbar--warning");
|
||||
}
|
||||
else if (_Options.status === "danger" || _Options.status === "error" || _Options.status === "red") {
|
||||
status.classList.add("js-snackbar--danger");
|
||||
}
|
||||
else {
|
||||
status.classList.add("js-snackbar--info");
|
||||
}
|
||||
|
||||
innerSnack.appendChild(status);
|
||||
}
|
||||
|
||||
_Message = document.createElement("span");
|
||||
_Message.classList.add("js-snackbar__message");
|
||||
_Message.textContent = _Options.message;
|
||||
|
||||
innerSnack.appendChild(_Message);
|
||||
|
||||
if (_Options.dismissible) {
|
||||
let closeBtn = document.createElement("span");
|
||||
closeBtn.classList.add("js-snackbar__close");
|
||||
closeBtn.innerText = "\u00D7";
|
||||
|
||||
closeBtn.onclick = snackbar.Close;
|
||||
|
||||
innerSnack.appendChild(closeBtn);
|
||||
}
|
||||
|
||||
_Element.style.height = "0px";
|
||||
_Element.style.opacity = "0";
|
||||
_Element.style.marginTop = "0px";
|
||||
_Element.style.marginBottom = "0px";
|
||||
|
||||
_Element.appendChild(innerSnack);
|
||||
_Container.appendChild(_Element);
|
||||
|
||||
if (_Options.timeout !== false) {
|
||||
_Interval = setTimeout(snackbar.Close, _Options.timeout);
|
||||
}
|
||||
}
|
||||
|
||||
var _ConfigureDefaults = function() {
|
||||
// if no options given, revert to default
|
||||
if (userOptions === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (userOptions.message !== undefined) {
|
||||
_Options.message = userOptions.message;
|
||||
}
|
||||
|
||||
if (userOptions.dismissible !== undefined) {
|
||||
if (typeof (userOptions.dismissible) === "string") {
|
||||
_Options.dismissible = (userOptions.dismissible === "true");
|
||||
}
|
||||
else if (typeof (userOptions.dismissible) === "boolean") {
|
||||
_Options.dismissible = userOptions.dismissible;
|
||||
}
|
||||
else {
|
||||
console.debug("Invalid option provided for 'dismissable' [" + userOptions.dismissible + "] is of type " + (typeof userOptions.dismissible));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (userOptions.timeout !== undefined) {
|
||||
if (typeof (userOptions.timeout) === "boolean" && userOptions.timeout === false) {
|
||||
_Options.timeout = false;
|
||||
}
|
||||
else if (typeof (userOptions.timeout) === "string") {
|
||||
_Options.timeout = parseInt(userOptions.timeout);
|
||||
}
|
||||
|
||||
|
||||
if (typeof (userOptions.timeout) === "number") {
|
||||
if (userOptions.timeout === Infinity) {
|
||||
_Options.timeout = false;
|
||||
}
|
||||
else if (userOptions.timeout >= 0) {
|
||||
_Options.timeout = userOptions.timeout;
|
||||
}
|
||||
else {
|
||||
console.debug("Invalid timeout entered. Must be greater than or equal to 0.");
|
||||
}
|
||||
|
||||
_Options.timeout = userOptions.timeout;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
if (userOptions.status !== undefined) {
|
||||
_Options.status = userOptions.status;
|
||||
}
|
||||
}
|
||||
|
||||
snackbar.Open = function() {
|
||||
let contentHeight = _Element.firstElementChild.scrollHeight; // get the height of the content
|
||||
|
||||
_Element.style.height = contentHeight + "px";
|
||||
_Element.style.opacity = 1;
|
||||
_Element.style.marginTop = "5px";
|
||||
_Element.style.marginBottom = "5px";
|
||||
|
||||
_Element.addEventListener("transitioned", function() {
|
||||
_Element.removeEventListener("transitioned", arguments.callee);
|
||||
_Element.style.height = null;
|
||||
})
|
||||
}
|
||||
|
||||
snackbar.Close = function () {
|
||||
if (_Interval)
|
||||
clearInterval(_Interval);
|
||||
|
||||
let snackbarHeight = _Element.scrollHeight; // get the auto height as a px value
|
||||
let snackbarTransitions = _Element.style.transition;
|
||||
_Element.style.transition = "";
|
||||
|
||||
requestAnimationFrame(function() {
|
||||
_Element.style.height = snackbarHeight + "px"; // set the auto height to the px height
|
||||
_Element.style.opacity = 1;
|
||||
_Element.style.marginTop = "0px";
|
||||
_Element.style.marginBottom = "0px";
|
||||
_Element.style.transition = snackbarTransitions
|
||||
|
||||
requestAnimationFrame(function() {
|
||||
_Element.style.height = "0px";
|
||||
_Element.style.opacity = 0;
|
||||
})
|
||||
});
|
||||
|
||||
setTimeout(function() {
|
||||
_Container.removeChild(_Element);
|
||||
}, 1000);
|
||||
};
|
||||
|
||||
_ConfigureDefaults();
|
||||
_Create();
|
||||
snackbar.Open();
|
||||
}
|
||||
|
||||
export function notify(scope){
|
||||
return function notify(str,opts){
|
||||
str = String(str)
|
||||
opts = opts || {}
|
||||
if( !opts.status ){
|
||||
opts.status = "info"
|
||||
if( str.match(/error/g) ) opts.status = "danger"
|
||||
if( str.match(/warning/g) ) opts.status = "warning"
|
||||
}
|
||||
opts = Object.assign({ message: str , status, timeout:2000 },opts)
|
||||
SnackBar( opts )
|
||||
}
|
||||
}
|
||||
638
example/assets/other.gltf
Normal file
638
example/assets/other.gltf
Normal file
File diff suppressed because one or more lines are too long
2153
example/assets/src_selfreference.gltf
Normal file
2153
example/assets/src_selfreference.gltf
Normal file
File diff suppressed because one or more lines are too long
|
|
@ -1,123 +0,0 @@
|
|||
:root {
|
||||
--primary: #6839dc;
|
||||
--light-primary: #ea23cf;
|
||||
--secondary: #872eff;
|
||||
--light-secondary: #ce7df2;
|
||||
--red: red;
|
||||
--black: #424280;
|
||||
--white: #fdfdfd;
|
||||
--dark-gray: #343334;
|
||||
--gray: #ecf7ff47;
|
||||
--light-gray: #efefef;
|
||||
--lighter-gray: #e4e2fb96;
|
||||
--font-sans-serif: system-ui, -apple-system, segoe ui, roboto, ubuntu, helvetica, cantarell, noto sans, sans-serif;
|
||||
--font-monospace: menlo, monaco, lucida console, liberation mono, dejavu sans mono, bitstream vera sans mono, courier new, monospace, serif;
|
||||
--border-radius: 0.2rem;
|
||||
}
|
||||
|
||||
.small{
|
||||
font-size:12px;
|
||||
}
|
||||
|
||||
textarea, select, input[type="text"] {
|
||||
background: transparent; /* linear-gradient( var(--lighter-gray), var(--gray) ) !important; */
|
||||
}
|
||||
|
||||
input[type="submit"] {
|
||||
color: var(--light-gray);
|
||||
}
|
||||
|
||||
.title {
|
||||
border-bottom: 2px solid var(--secondary);
|
||||
padding-bottom: 20px;
|
||||
}
|
||||
|
||||
#overlay{
|
||||
background: #FFF;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 48px;
|
||||
box-shadow: 0px 0px 10px #0004;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
#overlay .logo{
|
||||
width: 92px;
|
||||
position: absolute;
|
||||
top: 7px;
|
||||
left: 93px;
|
||||
}
|
||||
|
||||
#overlay > input[type="submit"]{
|
||||
height: 32px;
|
||||
font-size: 14px;
|
||||
position: absolute;
|
||||
right: 9px;
|
||||
top: 8px;
|
||||
}
|
||||
|
||||
#overlay > #uri {
|
||||
height: 29px;
|
||||
font-size: 14px;
|
||||
position: absolute;
|
||||
left: 200px;
|
||||
top: 9px;
|
||||
max-width:550px;
|
||||
padding: 5px 0px 5px 5px;
|
||||
width: calc( 63% - 200px);
|
||||
background: #f0f0f0;
|
||||
border-color: #Ccc;
|
||||
}
|
||||
|
||||
a#source{
|
||||
position: absolute;
|
||||
bottom: 29px;
|
||||
right: 20px;
|
||||
color: #888;
|
||||
font-weight: bold;
|
||||
font-family: sans-serif;
|
||||
text-decoration: underline;
|
||||
z-index:2000;
|
||||
}
|
||||
|
||||
a#model{
|
||||
position: absolute;
|
||||
bottom: 29px;
|
||||
right: 130px;
|
||||
color: #888;
|
||||
font-weight: bold;
|
||||
font-family: sans-serif;
|
||||
text-decoration: underline;
|
||||
z-index:2000;
|
||||
}
|
||||
|
||||
.render {
|
||||
position:absolute;
|
||||
top:0;
|
||||
left:0;
|
||||
right:0;
|
||||
bottom:0;
|
||||
}
|
||||
|
||||
.lil-gui.autoPlace{
|
||||
right:0px !important;
|
||||
top:auto !important;
|
||||
bottom:0;
|
||||
}
|
||||
|
||||
#VRButton {
|
||||
margin-bottom:20vh;
|
||||
}
|
||||
|
||||
@media (max-width: 450px) {
|
||||
#uri{ display:none; }
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.lil-gui.root{
|
||||
top:auto !important;
|
||||
left:auto !important;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,46 +0,0 @@
|
|||
|
||||
// contentLoaders = {".gltf" : () => .....} and so on
|
||||
|
||||
export function loadFile(contentLoaders, multiple){
|
||||
return () => {
|
||||
let input = document.createElement('input');
|
||||
input.type = 'file';
|
||||
input.multiple = multiple;
|
||||
input.accept = Object.keys(contentLoaders).join(",");
|
||||
input.onchange = () => {
|
||||
let files = Array.from(input.files);
|
||||
let file = files.slice ? files[0] : files
|
||||
for( var i in contentLoaders ){
|
||||
let r = new RegExp('\\'+i+'$')
|
||||
if( file.name.match(r) ) return contentLoaders[i](file)
|
||||
}
|
||||
alert(file.name+" is not supported")
|
||||
};
|
||||
input.click();
|
||||
}
|
||||
}
|
||||
|
||||
export function setupConsole($console){
|
||||
$console.style.position = 'absolute'
|
||||
$console.style.display = 'block'
|
||||
$console.style.zIndex = 1000;
|
||||
$console.style.background = "transparent !important"
|
||||
$console.style.pointerEvents = 'none'
|
||||
$console.style.top = '70px'
|
||||
$console.style.padding = '5px 20px 25px 25px'
|
||||
$console.style.left = $console.style.right = $console.style.bottom = 0;
|
||||
$console.style.color = '#0008';
|
||||
$console.style.fontSize = '12px';
|
||||
$console.style.fontFamily = 'Courier'
|
||||
$console.style.border = '0'
|
||||
$console.innerHTML = "XRFRAGMENT CONSOLE OUTPUT:\n"
|
||||
|
||||
console.log = ( (log) => function(){
|
||||
let s = new Date().toISOString().substr(11).substr(0,8) + " " + ([...arguments]).join(" ").replace(/.*[0-9]: /,"")
|
||||
log(s)
|
||||
let lines = String($console.innerHTML + "\n"+s).split("\n")
|
||||
while( lines.length > 200 ) lines.shift()
|
||||
$console.innerHTML = lines.join("\n")
|
||||
$console.scrollTop = $console.scrollHeight;
|
||||
})(console.log.bind(console))
|
||||
}
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
<html>
|
||||
<head>
|
||||
<link rel="stylesheet" href="./assets/axist.min.css" />
|
||||
<link rel="stylesheet" href="./assets/style.css" />
|
||||
<link rel="stylesheet" href="./assets/css/axist.min.css" />
|
||||
<link rel="stylesheet" href="./assets/css/style.css" />
|
||||
</head>
|
||||
<body>
|
||||
<script src="./../dist/xrfragment.js"></script>
|
||||
|
|
|
|||
1
example/threejs/sandbox/example3.gltf
Symbolic link
1
example/threejs/sandbox/example3.gltf
Symbolic link
|
|
@ -0,0 +1 @@
|
|||
../../assets/example3.gltf
|
||||
|
|
@ -1,26 +1,20 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>three.js vr - sandbox</title>
|
||||
<title>THREE.js - xrfragment sandbox</title>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
|
||||
<link rel="stylesheet" href="./../../assets/axist.min.css" />
|
||||
<link type="text/css" rel="stylesheet" href="main.css">
|
||||
<link type="text/css" rel="stylesheet" href="./../../assets/style.css"/>
|
||||
<link rel="stylesheet" href="./../../assets/css/axist.min.css" />
|
||||
<link type="text/css" rel="stylesheet" href="./../../assets/css/style.css"/>
|
||||
</head>
|
||||
<body>
|
||||
<div id="overlay" x-data="{ urls: ['#pos=-1,1.6,10','#pos=-1,1.6,2'] }">
|
||||
<div id="overlay" x-data="{ urls: ['#pos=0,1.6,15','#pos=0,1.6,15&rot=0,360,0'] }">
|
||||
<img src="./../../assets/logo.png" class="logo"/>
|
||||
<input type="submit" value="load 3D asset"></input>
|
||||
<input type="text" id="uri" list="urls" value="#pos=-1,1.6,10" x-on:change="XRF.eval( $('#uri').value, XRF.getLastModel() )"/>
|
||||
<datalist id="urls" >
|
||||
<template x-for="url in urls">
|
||||
<option x-bind:value="url" selected></option>
|
||||
</template>
|
||||
</datalist>
|
||||
<input type="text" id="uri" value="" onchange="AFRAME.XRF.navigator.to( $('#uri').value )"/>
|
||||
</div>
|
||||
<a id="source" target="_blank" href="https://github.com/coderofsalvation/xrfragment/blob/main/example/threejs/sandbox/index.html#L92-L112">sourcecode</a>
|
||||
<a id="model" target="_blank" href="">⬇️ model</a>
|
||||
<a class="btn-foot" id="source" target="_blank" href="https://github.com/coderofsalvation/xrfragment/blob/main/example/threejs/sandbox/index.html#L92-L112">sourcecode</a>
|
||||
<a class="btn-foot" id="model" target="_blank" href="">⬇️ model</a>
|
||||
<textarea style="display:none"></textarea>
|
||||
|
||||
<script async src="./../../assets/alpine.min.js"></script>
|
||||
|
|
@ -41,7 +35,7 @@
|
|||
|
||||
import xrfragment from './../../../dist/xrfragment.three.js';
|
||||
|
||||
import { loadFile, setupConsole } from './../../assets/utils.js';
|
||||
import { loadFile, setupConsole, setupUrlBar, notify } from './../../assets/js/utils.js';
|
||||
import { RGBELoader } from 'three/addons/loaders/RGBELoader.js';
|
||||
import { Lensflare, LensflareElement } from 'three/addons/objects/Lensflare.js';
|
||||
import { BoxLineGeometry } from 'three/addons/geometries/BoxLineGeometry.js';
|
||||
|
|
@ -89,16 +83,23 @@
|
|||
|
||||
window.addEventListener( 'resize', onWindowResize );
|
||||
|
||||
let cameraRig = new THREE.Group()
|
||||
cameraRig.position.set( 0, 0, 0 );
|
||||
cameraRig.add(camera)
|
||||
scene.add(cameraRig)
|
||||
|
||||
|
||||
// enable XR fragments
|
||||
let XRF = xrfragment.init({
|
||||
THREE,
|
||||
camera,
|
||||
camera:cameraRig,
|
||||
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?
|
||||
})
|
||||
|
||||
|
||||
// optional: react/extend/hook into XR fragment
|
||||
XRF.env = (xrf,v,opts) => {
|
||||
let { mesh, model, camera, scene, renderer, THREE} = opts
|
||||
|
|
@ -111,6 +112,7 @@
|
|||
console.log("hello custom property 'foobar'")
|
||||
}
|
||||
|
||||
|
||||
// *TODO* lowhanging fruit: during XR fragments-query milestone, target objects using XR fragment queries
|
||||
// to provide jquery-ish interface for three.js)
|
||||
//
|
||||
|
|
@ -120,38 +122,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()
|
||||
}
|
||||
gltf.scene.position.y = 1.5
|
||||
gltf.scene.position.z = -4
|
||||
gltf.scene.rotation.y = -0.5
|
||||
gltf.scene.scale.x = gltf.scene.scale.y = gltf.scene.scale.z = 1;
|
||||
|
||||
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 = 'example2.gltf'
|
||||
let file = document.location.search.length > 2 ? document.location.search.substr(1) + document.location.hash : './../../assets/example3.gltf#pos=1,0,4&rot=0,-30,0'
|
||||
$('#model').setAttribute("href","./../../asset/"+file)
|
||||
loader.load( file, loadGLTF );
|
||||
XRF.navigator.to( file )
|
||||
|
||||
|
||||
// setup mouse controls
|
||||
|
|
@ -167,32 +140,33 @@
|
|||
//controls.maxPolarAngle = Math.PI / 2;
|
||||
//controls.target = new THREE.Vector3(0,1.6,0)
|
||||
|
||||
camera.position.set( -1, 1.6, 10 );
|
||||
//controls.update()
|
||||
//controls.update()
|
||||
|
||||
const geometry = new THREE.BufferGeometry();
|
||||
geometry.setFromPoints( [ new THREE.Vector3( 0, 0, 0 ), new THREE.Vector3( 0, 0, - 5 ) ] );
|
||||
|
||||
const controller1 = renderer.xr.getController( 0 );
|
||||
controller1.add( new THREE.Line( geometry ) );
|
||||
scene.add( controller1 );
|
||||
cameraRig.add( controller1 );
|
||||
|
||||
const controller2 = renderer.xr.getController( 1 );
|
||||
controller2.add( new THREE.Line( geometry ) );
|
||||
scene.add( controller2 );
|
||||
cameraRig.add( controller2 );
|
||||
|
||||
|
||||
const controllerModelFactory = new XRControllerModelFactory();
|
||||
|
||||
const controllerGrip1 = renderer.xr.getControllerGrip( 0 );
|
||||
controllerGrip1.add( controllerModelFactory.createControllerModel( controllerGrip1 ) );
|
||||
scene.add( controllerGrip1 );
|
||||
cameraRig.add( controllerGrip1 );
|
||||
|
||||
const controllerGrip2 = renderer.xr.getControllerGrip( 1 );
|
||||
controllerGrip2.add( controllerModelFactory.createControllerModel( controllerGrip2 ) );
|
||||
scene.add( controllerGrip2 );
|
||||
cameraRig.add( controllerGrip2 );
|
||||
|
||||
setupConsole( $('textarea') )
|
||||
|
||||
setupConsole()
|
||||
setupUrlBar( $('input#uri') )
|
||||
|
||||
// GUI
|
||||
|
||||
|
|
@ -203,21 +177,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
|
||||
|
|
@ -232,7 +197,13 @@
|
|||
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 );
|
||||
})
|
||||
|
||||
let fileLoaders = loadFile({
|
||||
".gltf": (file) => file.arrayBuffer().then( (data) => loader.parse( data, '', loadGLTF, console.error ) ),
|
||||
|
|
@ -264,8 +235,6 @@
|
|||
//torus.rotation.x = time * 0.4;
|
||||
//torus.rotation.y = time;
|
||||
|
||||
if( XRF.getLastModel() ) XRF.getLastModel().render(scene,camera)
|
||||
|
||||
//controls.update()
|
||||
renderer.render( scene, camera );
|
||||
stats.update();
|
||||
|
|
|
|||
|
|
@ -1,91 +0,0 @@
|
|||
body {
|
||||
margin: 0;
|
||||
background-color: #000;
|
||||
color: #fff;
|
||||
font-family: Monospace;
|
||||
font-size: 13px;
|
||||
line-height: 24px;
|
||||
overscroll-behavior: none;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #ff0;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
button {
|
||||
cursor: pointer;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
#info {
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
box-sizing: border-box;
|
||||
text-align: center;
|
||||
-moz-user-select: none;
|
||||
-webkit-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
pointer-events: none;
|
||||
z-index: 1; /* TODO Solve this in HTML */
|
||||
}
|
||||
|
||||
a, button, input, select {
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.lil-gui {
|
||||
z-index: 2 !important; /* TODO Solve this in HTML */
|
||||
}
|
||||
|
||||
@media all and ( max-width: 640px ) {
|
||||
.lil-gui.root {
|
||||
right: auto;
|
||||
top: auto;
|
||||
max-height: 50%;
|
||||
max-width: 80%;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
#overlay {
|
||||
position: absolute;
|
||||
font-size: 16px;
|
||||
z-index: 2;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
background: rgba(0,0,0,0.7);
|
||||
}
|
||||
|
||||
#overlay button {
|
||||
background: transparent;
|
||||
border: 0;
|
||||
border: 1px solid rgb(255, 255, 255);
|
||||
border-radius: 4px;
|
||||
color: #ffffff;
|
||||
padding: 12px 18px;
|
||||
text-transform: uppercase;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#notSupported {
|
||||
width: 50%;
|
||||
margin: auto;
|
||||
background-color: #f00;
|
||||
margin-top: 20px;
|
||||
padding: 10px;
|
||||
}
|
||||
1
example/threejs/sandbox/other.gltf
Symbolic link
1
example/threejs/sandbox/other.gltf
Symbolic link
|
|
@ -0,0 +1 @@
|
|||
../../assets/other.gltf
|
||||
20
index.html
20
index.html
File diff suppressed because one or more lines are too long
16
make
16
make
|
|
@ -50,11 +50,21 @@ build(){
|
|||
|
||||
build_js(){
|
||||
# add js module
|
||||
cp dist/xrfragment.js dist/xrfragment.module.js
|
||||
echo "export default xrfragment;" >> dist/xrfragment.module.js
|
||||
cat dist/license.js dist/xrfragment.js > dist/xrfragment.module.js
|
||||
echo "export default xrfragment;" >> dist/xrfragment.module.js
|
||||
# add THREE module
|
||||
cat dist/xrfragment.js src/three/*.js src/three/xrf/*.js > dist/xrfragment.three.js
|
||||
cat dist/license.js \
|
||||
dist/xrfragment.js \
|
||||
src/3rd/three/*.js \
|
||||
src/3rd/three/xrf/*.js > dist/xrfragment.three.js
|
||||
echo "export default xrfragment;" >> dist/xrfragment.three.js
|
||||
# add AFRAME
|
||||
cat dist/license.js \
|
||||
dist/xrfragment.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
|
||||
}
|
||||
|
||||
|
|
|
|||
63
src/3rd/aframe/index.js
Normal file
63
src/3rd/aframe/index.js
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
window.AFRAME.registerComponent('xrf', {
|
||||
schema: {
|
||||
},
|
||||
init: function () {
|
||||
if( !AFRAME.XRF ) this.initXRFragments()
|
||||
if( this.data ){
|
||||
AFRAME.XRF.navigator.to(this.data)
|
||||
.then( (model) => {
|
||||
let gets = [ ...document.querySelectorAll('[xrf-get]') ]
|
||||
gets.map( (g) => g.emit('update') )
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
initXRFragments: function(){
|
||||
|
||||
//window.addEventListener('popstate', clear )
|
||||
//window.addEventListener('pushstate', clear )
|
||||
|
||||
// enable XR fragments
|
||||
let aScene = document.querySelector('a-scene')
|
||||
let XRF = AFRAME.XRF = xrfragment.init({
|
||||
THREE,
|
||||
camera: aScene.camera,
|
||||
scene: aScene.object3D,
|
||||
renderer: aScene.renderer,
|
||||
debug: true,
|
||||
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 camera-related XR Fragments so the camera-rig is affected
|
||||
let camOverride = (xrf,v,opts) => {
|
||||
opts.camera = document.querySelector('[camera]').object3D.parent
|
||||
console.dir(opts.camera)
|
||||
xrf(v,opts)
|
||||
}
|
||||
|
||||
XRF.pos = camOverride
|
||||
XRF.rot = camOverride
|
||||
|
||||
XRF.href = (xrf,v,opts) => { // convert portal to a-entity so AFRAME
|
||||
camOverride(xrf,v,opts) // raycaster can find & execute it
|
||||
let {mesh,camera} = opts;
|
||||
let el = document.createElement("a-entity")
|
||||
el.setAttribute("xrf-get",mesh.name )
|
||||
el.setAttribute("class","ray")
|
||||
el.addEventListener("click", mesh.userData.XRF.href.exec )
|
||||
$('a-scene').appendChild(el)
|
||||
}
|
||||
|
||||
// cleanup xrf-get objects when resetting scene
|
||||
XRF.reset = ((reset) => () => {
|
||||
reset()
|
||||
console.log("aframe reset")
|
||||
let els = [...document.querySelectorAll('[xrf-get]')]
|
||||
els.map( (el) => document.querySelector('a-scene').removeChild(el) )
|
||||
})(XRF.reset)
|
||||
|
||||
// undo lookup-control shenanigans (which blocks updating camerarig position in VR)
|
||||
aScene.addEventListener('enter-vr', () => document.querySelector('[camera]').object3D.parent.matrixAutoUpdate = true )
|
||||
},
|
||||
})
|
||||
108
src/3rd/aframe/xrf-button.js
Normal file
108
src/3rd/aframe/xrf-button.js
Normal file
|
|
@ -0,0 +1,108 @@
|
|||
window.AFRAME.registerComponent('xrf-button', {
|
||||
schema: {
|
||||
label: {
|
||||
default: 'label'
|
||||
},
|
||||
width: {
|
||||
default: 0.11
|
||||
},
|
||||
toggable: {
|
||||
default: false
|
||||
},
|
||||
textSize: {
|
||||
default: 0.66
|
||||
},
|
||||
color:{
|
||||
default: '#111'
|
||||
},
|
||||
textColor:{
|
||||
default: '#fff'
|
||||
},
|
||||
hicolor:{
|
||||
default: '#555555'
|
||||
},
|
||||
action:{
|
||||
default: ''
|
||||
}
|
||||
},
|
||||
init: function() {
|
||||
var el = this.el;
|
||||
var labelEl = this.labelEl = document.createElement('a-entity');
|
||||
this.color = this.data.color
|
||||
el.setAttribute('geometry', {
|
||||
primitive: 'box',
|
||||
width: this.data.width,
|
||||
height: 0.05,
|
||||
depth: 0.005
|
||||
});
|
||||
el.setAttribute('material', {
|
||||
color: this.color,
|
||||
transparent:true,
|
||||
opacity:0.3
|
||||
});
|
||||
el.setAttribute('pressable', '');
|
||||
labelEl.setAttribute('position', '0 0 0.01');
|
||||
labelEl.setAttribute('text', {
|
||||
value: this.data.label,
|
||||
color: this.data.textColor,
|
||||
align: 'center'
|
||||
});
|
||||
labelEl.setAttribute('scale', `${this.data.textSize} ${this.data.textSize} ${this.data.textSize}`);
|
||||
this.el.appendChild(labelEl);
|
||||
this.bindMethods();
|
||||
this.el.addEventListener('stateadded', this.stateChanged);
|
||||
this.el.addEventListener('stateremoved', this.stateChanged);
|
||||
this.el.addEventListener('pressedstarted', this.onPressedStarted);
|
||||
this.el.addEventListener('pressedended', this.onPressedEnded);
|
||||
this.el.addEventListener('mouseenter', (e) => this.onMouseEnter(e) );
|
||||
this.el.addEventListener('mouseleave', (e) => this.onMouseLeave(e) );
|
||||
|
||||
if( this.data.action ){
|
||||
this.el.addEventListener('click', new Function(this.data.action) )
|
||||
}
|
||||
},
|
||||
bindMethods: function() {
|
||||
this.stateChanged = this.stateChanged.bind(this);
|
||||
this.onPressedStarted = this.onPressedStarted.bind(this);
|
||||
this.onPressedEnded = this.onPressedEnded.bind(this);
|
||||
},
|
||||
update: function(oldData) {
|
||||
if (oldData.label !== this.data.label) {
|
||||
this.labelEl.setAttribute('text', 'value', this.data.label);
|
||||
}
|
||||
},
|
||||
stateChanged: function() {
|
||||
var color = this.el.is('pressed') ? this.data.hicolor : this.color;
|
||||
this.el.setAttribute('material', {
|
||||
color: color
|
||||
});
|
||||
},
|
||||
onMouseEnter: function(){
|
||||
this.el.setAttribute('material', { color: this.data.hicolor });
|
||||
},
|
||||
onMouseLeave: function(){
|
||||
this.el.setAttribute('material', { color: this.color });
|
||||
},
|
||||
onPressedStarted: function() {
|
||||
var el = this.el;
|
||||
el.setAttribute('material', {
|
||||
color: this.data.hicolor
|
||||
});
|
||||
el.emit('click');
|
||||
if (this.data.togabble) {
|
||||
if (el.is('pressed')) {
|
||||
el.removeState('pressed');
|
||||
} else {
|
||||
el.addState('pressed');
|
||||
}
|
||||
}
|
||||
},
|
||||
onPressedEnded: function() {
|
||||
if (this.el.is('pressed')) {
|
||||
return;
|
||||
}
|
||||
this.el.setAttribute('material', {
|
||||
color: this.color
|
||||
});
|
||||
}
|
||||
});
|
||||
35
src/3rd/aframe/xrf-get.js
Normal file
35
src/3rd/aframe/xrf-get.js
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
window.AFRAME.registerComponent('xrf-get', {
|
||||
schema: {
|
||||
name: {type: 'string'},
|
||||
clone: {type: 'boolean', default:false}
|
||||
},
|
||||
|
||||
init: function () {
|
||||
|
||||
var el = this.el;
|
||||
var meshname = this.data.name || this.data;
|
||||
|
||||
this.el.addEventListener('update', (evt) => {
|
||||
|
||||
let scene = AFRAME.XRF.scene
|
||||
let mesh = scene.getObjectByName(meshname);
|
||||
if (!mesh){
|
||||
console.error("mesh with name '"+meshname+"' not found in model")
|
||||
return;
|
||||
}
|
||||
if( !this.data.clone ) mesh.parent.remove(mesh)
|
||||
////mesh.updateMatrixWorld();
|
||||
this.el.object3D.position.setFromMatrixPosition(scene.matrixWorld);
|
||||
this.el.object3D.quaternion.setFromRotationMatrix(scene.matrixWorld);
|
||||
mesh.xrf = true // mark for deletion by xrf
|
||||
this.el.setObject3D('mesh', mesh );
|
||||
if( !this.el.id ) this.el.setAttribute("id",`xrf-${mesh.name}`)
|
||||
|
||||
})
|
||||
|
||||
if( this.el.className == "ray" ) this.el.emit("update")
|
||||
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
26
src/3rd/aframe/xrf-wear.js
Normal file
26
src/3rd/aframe/xrf-wear.js
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
window.AFRAME.registerComponent('xrf-wear', {
|
||||
schema:{
|
||||
el: {type:"selector"},
|
||||
position: {type:"vec3"},
|
||||
rotation: {type:"vec3"}
|
||||
},
|
||||
init: function(){
|
||||
$('a-scene').addEventListener('enter-vr', (e) => this.wear(e) )
|
||||
$('a-scene').addEventListener('exit-vr', (e) => this.unwear(e) )
|
||||
},
|
||||
wear: function(){
|
||||
if( !this.wearable ){
|
||||
let d = this.data
|
||||
this.wearable = new THREE.Group()
|
||||
this.el.object3D.children.map( (c) => this.wearable.add(c) )
|
||||
this.wearable.position.set( d.position.x, d.position.y, d.position.z)
|
||||
this.wearable.rotation.set( d.rotation.x, d.rotation.y, d.rotation.z)
|
||||
}
|
||||
this.data.el.object3D.add(this.wearable)
|
||||
},
|
||||
unwear: function(){
|
||||
this.data.el.remove(this.wearable)
|
||||
this.wearable.children.map( (c) => this.el.object3D.add(c) )
|
||||
delete this.wearable
|
||||
}
|
||||
})
|
||||
136
src/3rd/three/InteractiveGroup.js
Normal file
136
src/3rd/three/InteractiveGroup.js
Normal file
|
|
@ -0,0 +1,136 @@
|
|||
// 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
|
||||
|
||||
// extract camera when camera-rig is passed
|
||||
camera.traverse( (n) => String(n.type).match(/Camera/) ? camera = n : null )
|
||||
|
||||
const scope = this;
|
||||
|
||||
const raycaster = new Raycaster();
|
||||
const tempMatrix = new Matrix4();
|
||||
|
||||
function nocollide(){
|
||||
if( nocollide.tid ) return // ratelimit
|
||||
_event.type = "nocollide"
|
||||
scope.children.map( (c) => c.dispatchEvent(_event) )
|
||||
nocollide.tid = setTimeout( () => nocollide.tid = null, 100 )
|
||||
}
|
||||
|
||||
// Pointer Events
|
||||
|
||||
const element = renderer.domElement;
|
||||
|
||||
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 );
|
||||
|
||||
}else nocollide()
|
||||
|
||||
}
|
||||
|
||||
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 );
|
||||
|
||||
}else nocollide()
|
||||
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
144
src/3rd/three/index.js
Normal file
144
src/3rd/three/index.js
Normal file
|
|
@ -0,0 +1,144 @@
|
|||
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 ) 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.navigator.init()
|
||||
return xrf
|
||||
}
|
||||
|
||||
xrf.patchRenderer = function(renderer){
|
||||
renderer.xr.addEventListener( 'sessionstart', () => xrf.baseReferenceSpace = renderer.xr.getReferenceSpace() );
|
||||
renderer.xr.enabled = true;
|
||||
renderer.render = ((render) => function(scene,camera){
|
||||
if( xrf.model && xrf.model.render )
|
||||
xrf.model.render(scene,camera)
|
||||
render(scene,camera)
|
||||
})(renderer.render.bind(renderer))
|
||||
}
|
||||
|
||||
xrf.patchLoader = function(loader){
|
||||
loader.prototype.load = ((load) => function(url, onLoad, onProgress, onError){
|
||||
load.call( this,
|
||||
url,
|
||||
(model) => { onLoad(model); xrf.parseModel(model,url) },
|
||||
onProgress,
|
||||
onError)
|
||||
})(loader.prototype.load)
|
||||
}
|
||||
|
||||
xrf.getFile = (url) => url.split("/").pop().replace(/#.*/,'')
|
||||
|
||||
xrf.parseModel = function(model,url){
|
||||
let file = xrf.getFile(url)
|
||||
model.file = file
|
||||
model.render = function(){}
|
||||
// eval embedded XR fragments
|
||||
model.scene.traverse( (mesh) => xrf.eval.mesh(mesh,model) )
|
||||
}
|
||||
|
||||
xrf.getLastModel = () => xrf.model.last
|
||||
|
||||
xrf.eval = function( url, model ){
|
||||
let notice = false
|
||||
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: 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 }
|
||||
mesh.userData.XRF = frag // allow fragment impl to access XRF obj already
|
||||
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 = () => {
|
||||
const disposeObject = (obj) => {
|
||||
if (obj.children.length > 0) obj.children.forEach((child) => disposeObject(child));
|
||||
if (obj.geometry) obj.geometry.dispose();
|
||||
if (obj.material) {
|
||||
if (obj.material.map) obj.material.map.dispose();
|
||||
obj.material.dispose();
|
||||
}
|
||||
obj.clear()
|
||||
obj.removeFromParent()
|
||||
return true
|
||||
};
|
||||
let nodes = []
|
||||
xrf.scene.traverse( (child) => child.isXRF ? nodes.push(child) : false )
|
||||
nodes.map( disposeObject ) // leave non-XRF objects intact
|
||||
xrf.interactive = xrf.InteractiveGroup( xrf.THREE, xrf.renderer, xrf.camera)
|
||||
xrf.add( xrf.interactive)
|
||||
}
|
||||
|
||||
xrf.parseUrl = (url) => {
|
||||
const urlObj = new URL( url.match(/:\/\//) ? url : String(`https://fake.com/${url}`).replace(/\/\//,'/') )
|
||||
let dir = url.substring(0, url.lastIndexOf('/') + 1)
|
||||
const file = urlObj.pathname.substring(urlObj.pathname.lastIndexOf('/') + 1);
|
||||
const hash = url.match(/#/) ? url.replace(/.*#/,'') : ''
|
||||
const ext = file.split('.').pop()
|
||||
return {urlObj,dir,file,hash,ext}
|
||||
}
|
||||
|
||||
xrf.add = (object) => {
|
||||
object.isXRF = true // mark for easy deletion when replacing scene
|
||||
xrf.scene.add(object)
|
||||
}
|
||||
|
||||
/*
|
||||
* EVENTS
|
||||
*/
|
||||
|
||||
xrf.addEventListener = function(eventName, callback) {
|
||||
if( !this._listeners ) this._listeners = []
|
||||
if (!this._listeners[eventName]) {
|
||||
// create a new array for this event name if it doesn't exist yet
|
||||
this._listeners[eventName] = [];
|
||||
}
|
||||
// add the callback to the listeners array for this event name
|
||||
this._listeners[eventName].push(callback);
|
||||
};
|
||||
|
||||
xrf.emit = function(eventName, data) {
|
||||
if( !this._listeners ) this._listeners = []
|
||||
var callbacks = this._listeners[eventName]
|
||||
if (callbacks) {
|
||||
for (var i = 0; i < callbacks.length; i++) {
|
||||
callbacks[i](data);
|
||||
}
|
||||
}
|
||||
};
|
||||
44
src/3rd/three/navigator.js
Normal file
44
src/3rd/three/navigator.js
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
xrf.navigator = {}
|
||||
|
||||
xrf.navigator.to = (url,event) => {
|
||||
if( !url ) throw 'xrf.navigator.to(..) no url given'
|
||||
return new Promise( (resolve,reject) => {
|
||||
let {urlObj,dir,file,hash,ext} = xrf.parseUrl(url)
|
||||
console.log("xrfragment: navigating to "+url)
|
||||
|
||||
if( !file || xrf.model.file == file ){ // we're already loaded
|
||||
document.location.hash = `#${hash}` // just update the hash
|
||||
xrf.eval( url, xrf.model ) // and eval URI XR fragments
|
||||
return resolve(xrf.model)
|
||||
}
|
||||
|
||||
if( xrf.model && xrf.model.scene ) xrf.model.scene.visible = false
|
||||
const Loader = xrf.loaders[ext]
|
||||
if( !Loader ) throw 'xrfragment: no loader passed to xrfragment for extension .'+ext
|
||||
xrf.reset() // clear xrf objects from scene
|
||||
// force relative path
|
||||
if( dir ) dir = dir[0] == '.' ? dir : `.${dir}`
|
||||
const loader = new Loader().setPath( dir )
|
||||
loader.load( file, function(model){
|
||||
model.file = file
|
||||
xrf.add( model.scene )
|
||||
xrf.model = model
|
||||
xrf.eval( url, model ) // and eval URI XR fragments
|
||||
xrf.navigator.pushState( file, hash )
|
||||
resolve(model)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
xrf.navigator.init = () => {
|
||||
if( xrf.navigator.init.inited ) return
|
||||
window.addEventListener('popstate', function (event){
|
||||
xrf.navigator.to( document.location.search.substr(1) + document.location.hash, event)
|
||||
})
|
||||
xrf.navigator.init.inited = true
|
||||
}
|
||||
|
||||
xrf.navigator.pushState = (file,hash) => {
|
||||
if( file == document.location.search.substr(1) ) return // page is in its default state
|
||||
window.history.pushState({},`${file}#${hash}`, document.location.pathname + `?${file}#${hash}` )
|
||||
}
|
||||
35
src/3rd/three/xrf/env.js
Normal file
35
src/3rd/three/xrf/env.js
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
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
|
||||
renderer.toneMapping = THREE.ACESFilmicToneMapping;
|
||||
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`)
|
||||
}
|
||||
152
src/3rd/three/xrf/href.js
Normal file
152
src/3rd/three/xrf/href.js
Normal file
|
|
@ -0,0 +1,152 @@
|
|||
/**
|
||||
*
|
||||
* navigation, portals & mutations
|
||||
*
|
||||
* | fragment | type | scope | example value |
|
||||
* |`href`| string (uri or predefined view) | 🔒 |`#pos=1,1,0`<br>`#pos=1,1,0&rot=90,0,0`<br>`#pos=pyramid`<br>`#pos=lastvisit\|pyramid`<br>`://somefile.gltf#pos=1,1,0`<br> |
|
||||
*
|
||||
* [[» example implementation|https://github.com/coderofsalvation/xrfragment/blob/main/src/three/xrf/pos.js]]<br>
|
||||
* [[» example 3D asset|https://github.com/coderofsalvation/xrfragment/blobl/main/src/example/assets/href.gltf]]<br>
|
||||
* [[» discussion|https://github.com/coderofsalvation/xrfragment/issues/1]]<br>
|
||||
*
|
||||
* [img[xrfragment.jpg]]
|
||||
*
|
||||
*
|
||||
* !!!spec 1.0
|
||||
*
|
||||
* 1. an ''external''- or ''file URI'' fully replaces the current scene and assumes `pos=0,0,0&rot=0,0,0` by default (unless specified)
|
||||
*
|
||||
* 2. navigation should not happen when queries (`q=`) are present in local url: queries will apply (`pos=`, `rot=` e.g.) to the targeted object(s) instead.
|
||||
*
|
||||
* 3. navigation should not happen ''immediately'' when user is more than 2 meter away from the portal/object containing the href (to prevent accidental navigation e.g.)
|
||||
*
|
||||
* 4. URL navigation should always be reflected in the client (in case of javascript: see [[here|https://github.com/coderofsalvation/xrfragment/blob/dev/src/3rd/three/navigator.js]] for an example navigator).
|
||||
*
|
||||
* 5. In XR mode, the navigator back/forward-buttons should be always visible (using a wearable e.g., see [[here|https://github.com/coderofsalvation/xrfragment/blob/dev/example/aframe/sandbox/index.html#L26-L29]] for an example wearable)
|
||||
*
|
||||
* [img[navigation.png]]
|
||||
*
|
||||
*/
|
||||
|
||||
xrf.frag.href = function(v, opts){
|
||||
let { mesh, model, camera, scene, renderer, THREE} = opts
|
||||
|
||||
const world = {
|
||||
pos: new THREE.Vector3(),
|
||||
scale: new THREE.Vector3(),
|
||||
quat: new THREE.Quaternion()
|
||||
}
|
||||
mesh.getWorldPosition(world.pos)
|
||||
mesh.getWorldScale(world.scale)
|
||||
mesh.getWorldQuaternion(world.quat);
|
||||
mesh.position.copy(world.pos)
|
||||
mesh.scale.copy(world.scale)
|
||||
mesh.setRotationFromQuaternion(world.quat);
|
||||
|
||||
// detect equirectangular image
|
||||
let texture = mesh.material.map
|
||||
if( texture && texture.source.data.height == texture.source.data.width/2 ){
|
||||
texture.mapping = THREE.ClampToEdgeWrapping
|
||||
texture.needsUpdate = true
|
||||
|
||||
// poor man's equi-portal
|
||||
mesh.material = new THREE.ShaderMaterial( {
|
||||
side: THREE.DoubleSide,
|
||||
uniforms: {
|
||||
pano: { value: texture },
|
||||
selected: { value: false },
|
||||
},
|
||||
vertexShader: `
|
||||
vec3 portalPosition;
|
||||
varying vec3 vWorldPosition;
|
||||
varying float vDistanceToCenter;
|
||||
varying float vDistance;
|
||||
void main() {
|
||||
vDistanceToCenter = clamp(length(position - vec3(0.0, 0.0, 0.0)), 0.0, 1.0);
|
||||
portalPosition = (modelMatrix * vec4(0.0, 0.0, 0.0, 1.0)).xyz;
|
||||
vDistance = length(portalPosition - cameraPosition);
|
||||
vWorldPosition = (modelMatrix * vec4(position, 1.0)).xyz;
|
||||
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
|
||||
}
|
||||
`,
|
||||
fragmentShader: `
|
||||
#define RECIPROCAL_PI2 0.15915494
|
||||
uniform sampler2D pano;
|
||||
uniform bool selected;
|
||||
varying float vDistanceToCenter;
|
||||
varying float vDistance;
|
||||
varying vec3 vWorldPosition;
|
||||
void main() {
|
||||
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;
|
||||
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 = selected ? color : vec4(vec3(luminance) + vec3(0.33), color.a);
|
||||
gl_FragColor = grayscale_color;
|
||||
}
|
||||
`,
|
||||
});
|
||||
mesh.material.needsUpdate = true
|
||||
}
|
||||
|
||||
let teleport = mesh.userData.XRF.href.exec = (e) => {
|
||||
const meshWorldPosition = new THREE.Vector3();
|
||||
meshWorldPosition.setFromMatrixPosition(mesh.matrixWorld);
|
||||
|
||||
let portalArea = 1 // 2 meter
|
||||
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 distance = camera.position.distanceTo(newPos);
|
||||
//if( distance > portalArea ){
|
||||
if( !renderer.xr.isPresenting && !confirm("teleport to "+v.string+" ?") ) return
|
||||
|
||||
xrf.navigator.to(v.string) // ok let's surf to HREF!
|
||||
console.log("teleport!")
|
||||
|
||||
xrf.emit('href',{click:true,mesh,xrf:v})
|
||||
}
|
||||
|
||||
let selected = (state) => () => {
|
||||
if( mesh.selected == state ) return // nothing changed
|
||||
if( mesh.material.uniforms ) mesh.material.uniforms.selected.value = state
|
||||
else mesh.material.color.r = mesh.material.color.g = mesh.material.color.b = state ? 2.0 : 1.0
|
||||
// update mouse cursor
|
||||
if( !renderer.domElement.lastCursor )
|
||||
renderer.domElement.lastCursor = renderer.domElement.style.cursor
|
||||
renderer.domElement.style.cursor = state ? 'pointer' : renderer.domElement.lastCursor
|
||||
xrf.emit('href',{selected:state,mesh,xrf:v})
|
||||
mesh.selected = state
|
||||
}
|
||||
|
||||
if( !opts.frag.q ){ // query means an action
|
||||
mesh.addEventListener('click', teleport )
|
||||
mesh.addEventListener('mousemove', selected(true) )
|
||||
mesh.addEventListener('nocollide', selected(false) )
|
||||
}
|
||||
|
||||
// lazy add mesh (because we're inside a recursive traverse)
|
||||
setTimeout( (mesh) => {
|
||||
xrf.interactive.add(mesh)
|
||||
}, 20, mesh )
|
||||
}
|
||||
|
||||
/**
|
||||
* > above solutions were abducted from [[this|https://i.imgur.com/E3En0gJ.png]] and [[this|https://i.imgur.com/lpnTz3A.png]] survey result
|
||||
*
|
||||
* !!!Demo
|
||||
*
|
||||
* > taken from <a href="./example/aframe/sandbox" target="_blank">aframe/sandbox</a>
|
||||
*
|
||||
* <video width="100%" autoplay="" muted="" loop="">
|
||||
* <source src="https://coderofsalvation.github.io/xrfragment.media/href.mp4" type="video/mp4">Your browser does not support the video element.
|
||||
* </video>
|
||||
*/
|
||||
47
src/3rd/three/xrf/pos.js
Normal file
47
src/3rd/three/xrf/pos.js
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
xrf.frag.pos = function(v, opts){
|
||||
//if( renderer.xr.isPresenting ) return // too far away
|
||||
let { frag, mesh, model, camera, scene, renderer, THREE} = opts
|
||||
console.log(" └ setting camera position to "+v.string)
|
||||
|
||||
if( !frag.q ){
|
||||
|
||||
if( true ){//!renderer.xr.isPresenting ){
|
||||
console.dir(camera)
|
||||
camera.position.x = v.x
|
||||
camera.position.y = v.y
|
||||
camera.position.z = v.z
|
||||
}
|
||||
/*
|
||||
else{ // XR
|
||||
let cameraWorldPosition = new THREE.Vector3()
|
||||
camera.object3D.getWorldPosition(this.cameraWorldPosition)
|
||||
let newRigWorldPosition = new THREE.Vector3(v.x,v.y,x.z)
|
||||
|
||||
// Finally update the cameras position
|
||||
let newRigLocalPosition.copy(this.newRigWorldPosition)
|
||||
if (camera.object3D.parent) {
|
||||
camera.object3D.parent.worldToLocal(newRigLocalPosition)
|
||||
}
|
||||
camera.setAttribute('position', newRigLocalPosition)
|
||||
|
||||
// Also take the headset/camera rotation itself into account
|
||||
if (this.data.rotateOnTeleport) {
|
||||
this.teleportOcamerainQuaternion
|
||||
.setFromEuler(new THREE.Euler(0, this.teleportOcamerain.object3D.rotation.y, 0))
|
||||
this.teleportOcamerainQuaternion.invert()
|
||||
this.teleportOcamerainQuaternion.multiply(this.hitEntityQuaternion)
|
||||
// Rotate the camera based on calculated teleport ocamerain rotation
|
||||
this.cameraRig.object3D.setRotationFromQuaternion(this.teleportOcamerainQuaternion)
|
||||
}
|
||||
|
||||
console.log("XR")
|
||||
const offsetPosition = { x: - v.x, y: - v.y, z: - v.z, w: 1 };
|
||||
const offsetRotation = new THREE.Quaternion();
|
||||
const transform = new XRRigidTransform( offsetPosition, offsetRotation );
|
||||
const teleportSpaceOffset = xrf.baseReferenceSpace.getOffsetReferenceSpace( transform );
|
||||
renderer.xr.setReferenceSpace( teleportSpaceOffset );
|
||||
}
|
||||
*/
|
||||
|
||||
}
|
||||
}
|
||||
14
src/3rd/three/xrf/q.js
Normal file
14
src/3rd/three/xrf/q.js
Normal 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}`)
|
||||
}
|
||||
}
|
||||
9
src/3rd/three/xrf/rot.js
Normal file
9
src/3rd/three/xrf/rot.js
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
xrf.frag.rot = function(v, opts){
|
||||
let { mesh, model, camera, scene, renderer, THREE} = opts
|
||||
console.log(" └ setting camera rotation to "+v.string)
|
||||
camera.rotation.set(
|
||||
v.x * Math.PI / 180,
|
||||
v.y * Math.PI / 180,
|
||||
v.z * Math.PI / 180
|
||||
)
|
||||
}
|
||||
38
src/3rd/three/xrf/src.js
Normal file
38
src/3rd/three/xrf/src.js
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
// *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 frag = xrfragment.URI.parse(v.string)
|
||||
// Get an instance of the original model
|
||||
let sceneInstance = new THREE.Group()
|
||||
sceneInstance.isSrc = true
|
||||
|
||||
// prevent infinite recursion #1: skip src-instanced models
|
||||
for ( let i in model.scene.children ) {
|
||||
let child = model.scene.children[i]
|
||||
if( child.isSrc ) continue;
|
||||
sceneInstance.add( model.scene.children[i].clone() )
|
||||
}
|
||||
|
||||
sceneInstance.position.copy( mesh.position )
|
||||
sceneInstance.scale.copy(mesh.scale)
|
||||
sceneInstance.updateMatrixWorld(true) // needed because we're going to move portals to the interactive-group
|
||||
|
||||
// apply embedded XR fragments
|
||||
setTimeout( () => {
|
||||
sceneInstance.traverse( (m) => {
|
||||
if( m.userData && m.userData.src ) return ;//delete m.userData.src // prevent infinite recursion
|
||||
xrf.eval.mesh(m,{scene,recursive:true})
|
||||
})
|
||||
// apply URI XR Fragments inside src-value
|
||||
for( var i in frag ){
|
||||
xrf.eval.fragment(i, Object.assign(opts,{frag, model:{scene:sceneInstance},scene:sceneInstance}))
|
||||
}
|
||||
// Add the instance to the scene
|
||||
model.scene.add(sceneInstance);
|
||||
},10)
|
||||
}
|
||||
}
|
||||
2
src/3rd/wasm/README.md
Normal file
2
src/3rd/wasm/README.md
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
javy compile index.js -o index.wasm
|
||||
echo '{ "n": 2, "bar": "baz" }' | wasmtime index.wasm
|
||||
50
src/3rd/wasm/index.js
Normal file
50
src/3rd/wasm/index.js
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
// Read input from stdin
|
||||
const input = readInput();
|
||||
// Call the function with the input
|
||||
const result = foo(input);
|
||||
// Write the result to stdout
|
||||
writeOutput(result);
|
||||
|
||||
// The main function.
|
||||
function foo(input) {
|
||||
return { foo: input.n + 1, newBar: input.bar + "!" };
|
||||
}
|
||||
|
||||
// Read input from stdin
|
||||
function readInput() {
|
||||
const chunkSize = 1024;
|
||||
const inputChunks = [];
|
||||
let totalBytes = 0;
|
||||
|
||||
// Read all the available bytes
|
||||
while (1) {
|
||||
const buffer = new Uint8Array(chunkSize);
|
||||
// Stdin file descriptor
|
||||
const fd = 0;
|
||||
const bytesRead = Javy.IO.readSync(fd, buffer);
|
||||
|
||||
totalBytes += bytesRead;
|
||||
if (bytesRead === 0) {
|
||||
break;
|
||||
}
|
||||
inputChunks.push(buffer.subarray(0, bytesRead));
|
||||
}
|
||||
|
||||
// Assemble input into a single Uint8Array
|
||||
const { finalBuffer } = inputChunks.reduce((context, chunk) => {
|
||||
context.finalBuffer.set(chunk, context.bufferOffset);
|
||||
context.bufferOffset += chunk.length;
|
||||
return context;
|
||||
}, { bufferOffset: 0, finalBuffer: new Uint8Array(totalBytes) });
|
||||
|
||||
return JSON.parse(new TextDecoder().decode(finalBuffer));
|
||||
}
|
||||
|
||||
// Write output to stdout
|
||||
function writeOutput(output) {
|
||||
const encodedOutput = new TextEncoder().encode(JSON.stringify(output));
|
||||
const buffer = new Uint8Array(encodedOutput);
|
||||
// Stdout file descriptor
|
||||
const fd = 1;
|
||||
Javy.IO.writeSync(fd, buffer);
|
||||
}
|
||||
BIN
src/3rd/wasm/index.wasm
Normal file
BIN
src/3rd/wasm/index.wasm
Normal file
Binary file not shown.
|
|
@ -1,74 +0,0 @@
|
|||
xrfragment.xrf = {}
|
||||
xrfragment.model = {}
|
||||
|
||||
xrfragment.init = function(opts){
|
||||
opts = opts || {}
|
||||
let XRF = function(){
|
||||
alert("queries are not implemented (yet)")
|
||||
}
|
||||
for ( let i in opts ) XRF[i] = xrfragment[i] = opts[i]
|
||||
for ( let i in xrfragment ) XRF[i] = xrfragment[i]
|
||||
for ( let i in xrfragment.XRF ) XRF[i] = xrfragment.XRF[i] // shortcuts to constants (NAVIGATOR e.g.)
|
||||
xrfragment.Parser.debug = xrfragment.debug
|
||||
if( opts.loaders ) opts.loaders.map( xrfragment.patchLoader )
|
||||
return XRF
|
||||
}
|
||||
|
||||
xrfragment.patchLoader = function(loader){
|
||||
loader.prototype.load = ((load) => function(url, onLoad, onProgress, onError){
|
||||
load.call( this,
|
||||
url,
|
||||
(model) => { onLoad(model); xrfragment.parseModel(model,url) },
|
||||
onProgress,
|
||||
onError)
|
||||
})(loader.prototype.load)
|
||||
}
|
||||
|
||||
xrfragment.getFile = (url) => url.split("/").pop().replace(/#.*/,'')
|
||||
|
||||
xrfragment.parseModel = function(model,url){
|
||||
let file = xrfragment.getFile(url)
|
||||
model.file = file
|
||||
model.render = function(){}
|
||||
xrfragment.model[file] = model
|
||||
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 )
|
||||
// call native function (xrf/env.js e.g.), or pass it to user decorator
|
||||
let func = xrfragment.xrf[k] || function(){}
|
||||
let opts = {mesh, model, camera: xrfragment.camera, scene: xrfragment.scene, renderer: xrfragment.renderer, THREE: xrfragment.THREE }
|
||||
if( xrfragment[k] ) xrfragment[k]( func, frag[k], opts)
|
||||
else func( frag[k], opts)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
xrfragment.getLastModel = () => Object.values(xrfragment.model)[ Object.values(xrfragment.model).length-1 ]
|
||||
|
||||
xrfragment.eval = function( url, model ){
|
||||
let notice = false
|
||||
let { THREE, camera } = xrfragment
|
||||
let frag = xrfragment.URI.parse( url, XRF.NAVIGATOR )
|
||||
|
||||
for ( let i in frag ) {
|
||||
if( !String(i).match(/(pos|rot)/) ) notice = true
|
||||
if( i == "pos" ){
|
||||
camera.position.x = frag.pos.x
|
||||
camera.position.y = frag.pos.y
|
||||
camera.position.z = frag.pos.z
|
||||
}
|
||||
if( i == "rot" ){
|
||||
camera.rotation.x = THREE.MathUtils.degToRad( frag.pos.x )
|
||||
camera.rotation.y = THREE.MathUtils.degToRad( frag.pos.y )
|
||||
camera.rotation.z = THREE.MathUtils.degToRad( frag.pos.z )
|
||||
}
|
||||
}
|
||||
if( notice ) alert("only 'pos' and 'rot' XRF.NAVIGATOR-flagged XR fragments are supported (for now)")
|
||||
console.dir({url,model,frag})
|
||||
}
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
xrfragment.xrf.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
|
||||
renderer.toneMapping = THREE.ACESFilmicToneMapping;
|
||||
renderer.toneMappingExposure = 1;
|
||||
console.log(` └ applied image '${v.string}' as environtment map`)
|
||||
}
|
||||
|
|
@ -1,57 +0,0 @@
|
|||
xrfragment.xrf.href = function(v, opts){
|
||||
let { mesh, model, camera, scene, renderer, THREE} = opts
|
||||
return
|
||||
|
||||
// Create a shader material that treats the texture as an equirectangular map
|
||||
mesh.texture = mesh.material.map // backup texture
|
||||
const equirectShader = THREE.ShaderLib[ 'equirect' ];
|
||||
const equirectMaterial = new THREE.ShaderMaterial( {
|
||||
uniforms: THREE.UniformsUtils.merge([
|
||||
THREE.UniformsLib.equirect,
|
||||
equirectShader.uniforms,
|
||||
]),
|
||||
vertexShader: equirectShader.vertexShader,
|
||||
fragmentShader: equirectShader.fragmentShader,
|
||||
side: THREE.DoubleSide //THREE.FrontSide //THREE.DoubleSide //THREE.BackSide
|
||||
} );
|
||||
equirectMaterial.uniforms[ 'tEquirect' ].value = mesh.texture
|
||||
// Define the tEquirectInvProjection uniform
|
||||
equirectMaterial.uniforms.tEquirectInvProjection = {
|
||||
value: new THREE.Matrix4(),
|
||||
};
|
||||
// Assign the new material to the mesh
|
||||
mesh.material = equirectMaterial;
|
||||
console.dir(mesh.material)
|
||||
mesh.texture.wrapS = THREE.RepeatWrapping;
|
||||
|
||||
// patch custom model renderloop
|
||||
model.render = ((render) => (scene,camera) => {
|
||||
|
||||
// Store the original projection matrix of the camera
|
||||
const originalProjectionMatrix = camera.projectionMatrix.clone();
|
||||
// Calculate the current camera view matrix
|
||||
const aspectRatio = mesh.texture.image.width / mesh.texture.image.height;
|
||||
camera.projectionMatrix.makePerspective(camera.fov, aspectRatio, camera.near, camera.far);
|
||||
|
||||
const viewMatrix = camera.matrixWorldInverse;
|
||||
const worldMatrix = mesh.matrixWorld;
|
||||
|
||||
const equirectInvProjection = new THREE.Matrix4();
|
||||
equirectInvProjection.copy(camera.projectionMatrix).multiply(viewMatrix).invert();
|
||||
|
||||
// Update the equirectangular material's tEquirect uniform
|
||||
equirectMaterial.uniforms.tEquirect.value = mesh.texture;
|
||||
equirectMaterial.uniforms.tEquirectInvProjection.value.copy(
|
||||
equirectInvProjection
|
||||
);
|
||||
|
||||
// Reset the camera projection matrix
|
||||
camera.projectionMatrix.copy(originalProjectionMatrix);
|
||||
|
||||
|
||||
render(scene,camera)
|
||||
|
||||
})(model.render)
|
||||
|
||||
console.dir(mesh)
|
||||
}
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
xrfragment.xrf.pos = function(v, opts){
|
||||
let { 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
|
||||
}
|
||||
|
|
@ -1,31 +0,0 @@
|
|||
xrfragment.xrf.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)
|
||||
// Get an instance of the original model
|
||||
const modelInstance = new THREE.Group();
|
||||
modelInstance.add(model.scene.clone());
|
||||
modelInstance.position.z = mesh.position.x
|
||||
modelInstance.position.y = mesh.position.y
|
||||
modelInstance.position.x = mesh.position.z
|
||||
modelInstance.scale.z = mesh.scale.x
|
||||
modelInstance.scale.y = mesh.scale.y
|
||||
modelInstance.scale.x = mesh.scale.z
|
||||
// now apply XR Fragments overrides from URI
|
||||
// *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
|
||||
}
|
||||
}
|
||||
// Add the instance to the scene
|
||||
scene.add(modelInstance);
|
||||
console.dir(model)
|
||||
console.dir(modelInstance)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,3 +1,5 @@
|
|||
// SPDX-License-Identifier: MPL-2.0
|
||||
// Copyright (c) 2023 Leon van Kammen/NLNET
|
||||
package xrfragment;
|
||||
|
||||
import xrfragment.XRF;
|
||||
|
|
|
|||
|
|
@ -1,3 +1,6 @@
|
|||
-- SPDX-License-Identifier: MPL-2.0
|
||||
-- Copyright (c) 2023 Leon van Kammen/NLNET
|
||||
|
||||
XF = {}
|
||||
|
||||
function split (inputstr, sep)
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
// SPDX-License-Identifier: MPL-2.0
|
||||
// Copyright (c) 2023 Leon van Kammen/NLNET
|
||||
package xrfragment;
|
||||
|
||||
//return untyped __js__("window.location.search");
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
// SPDX-License-Identifier: MPL-2.0
|
||||
// Copyright (c) 2023 Leon van Kammen/NLNET
|
||||
package xrfragment;
|
||||
|
||||
import xrfragment.Parser;
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
// SPDX-License-Identifier: MPL-2.0
|
||||
// Copyright (c) 2023 Leon van Kammen/NLNET
|
||||
package xrfragment;
|
||||
|
||||
@:expose // <- makes the class reachable from plain JavaScript
|
||||
|
|
|
|||
|
|
@ -1,3 +1,6 @@
|
|||
-- SPDX-License-Identifier: MPL-2.0
|
||||
-- Copyright (c) 2023 Leon van Kammen/NLNET
|
||||
|
||||
local XRF = {}
|
||||
|
||||
XRF.ASSET = 1
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue