xrfragment-haxe/src/3rd/js/three/xrf/href.js

149 lines
6.4 KiB
JavaScript
Raw Normal View History

2023-05-22 13:56:33 +02:00
/**
2023-05-23 12:35:05 +02:00
*
2023-05-22 13:56:33 +02:00
* navigation, portals & mutations
2023-05-23 12:35:05 +02:00
*
2023-05-22 13:56:33 +02:00
* | fragment | type | scope | example value |
2023-05-23 13:00:28 +02:00
* |`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> |
2023-05-23 12:35:05 +02:00
*
2023-05-23 13:00:28 +02:00
* [[» example implementation|https://github.com/coderofsalvation/xrfragment/blob/main/src/3rd/three/xrf/href.js]]<br>
* [[» example 3D asset|https://github.com/coderofsalvation/xrfragment/blob/main/example/assets/href.gltf#L192]]<br>
2023-05-23 12:35:05 +02:00
* [[» discussion|https://github.com/coderofsalvation/xrfragment/issues/1]]<br>
2023-05-22 13:56:33 +02:00
*
2023-05-22 14:10:44 +02:00
* [img[xrfragment.jpg]]
2023-05-23 12:35:05 +02:00
*
*
2023-05-22 14:10:44 +02:00
* !!!spec 1.0
2023-05-23 12:35:05 +02:00
*
* 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)
*
2023-05-22 13:56:33 +02:00
* 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.
2023-05-23 12:35:05 +02:00
*
2023-05-22 14:10:44 +02:00
* 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.)
2023-05-23 12:35:05 +02:00
*
* 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]]
*
2023-05-22 13:56:33 +02:00
*/
xrf.frag.href = function(v, opts){
opts.embedded = v // indicate embedded XR fragment
2023-05-05 18:53:42 +02:00
let { mesh, model, camera, scene, renderer, THREE} = opts
2023-06-09 16:40:08 +02:00
if( mesh.userData.XRF.href.exec ) return // mesh already initialized
2023-05-22 13:56:33 +02:00
const world = {
pos: new THREE.Vector3(),
scale: new THREE.Vector3(),
quat: new THREE.Quaternion()
}
// detect equirectangular image
let texture = mesh.material && mesh.material.map ? mesh.material.map : null
2023-05-17 21:31:28 +02:00
if( texture && texture.source.data.height == texture.source.data.width/2 ){
texture.mapping = THREE.ClampToEdgeWrapping
texture.needsUpdate = true
2023-05-10 19:12:15 +02:00
2023-05-22 13:56:33 +02:00
// 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
}else if( mesh.material){ mesh.material = mesh.material.clone() }
2023-06-07 17:42:21 +02:00
let click = mesh.userData.XRF.href.exec = (e) => {
2023-06-22 13:59:17 +02:00
let isLocal = v.string[0] == '#'
let lastPos = `pos=${camera.position.x.toFixed(2)},${camera.position.y.toFixed(2)},${camera.position.z.toFixed(2)}`
xrf
.emit('href',{click:true,mesh,xrf:v}) // let all listeners agree
.then( () => {
2023-08-31 12:40:41 +02:00
const flags = v.string[0] == '#' ? xrf.XRF.PV_OVERRIDE : undefined
// always keep a trail of last positions before we navigate
if( !v.string.match(/pos=/) ) v.string += `${v.string[0] == '#' ? '&' : '#'}${lastPos}`
if( !document.location.hash.match(/pos=/) ) xrf.navigator.to(`#${lastPos}`,flags)
xrf.navigator.to(v.string) // let's surf to HREF!
})
}
2023-05-22 13:56:33 +02:00
let selected = (state) => () => {
if( mesh.selected == state ) return // nothing changed
if( mesh.material ){
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
}
2023-05-22 13:56:33 +02:00
// update mouse cursor
if( !renderer.domElement.lastCursor )
renderer.domElement.lastCursor = renderer.domElement.style.cursor
renderer.domElement.style.cursor = state ? 'pointer' : renderer.domElement.lastCursor
2023-06-02 12:00:21 +02:00
xrf
.emit('href',{selected:state,mesh,xrf:v}) // let all listeners agree
.then( () => mesh.selected = state )
2023-05-22 13:56:33 +02:00
}
2023-06-07 17:42:21 +02:00
mesh.addEventListener('click', click )
mesh.addEventListener('mousemove', selected(true) )
mesh.addEventListener('nocollide', selected(false) )
2023-05-17 21:31:28 +02:00
2023-05-22 13:56:33 +02:00
// lazy add mesh (because we're inside a recursive traverse)
setTimeout( (mesh) => {
2023-06-07 17:42:21 +02:00
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);
2023-05-22 13:56:33 +02:00
xrf.interactive.add(mesh)
}, 10, mesh )
2023-05-05 18:53:42 +02:00
}
2023-05-22 13:56:33 +02:00
/**
2023-05-23 12:35:05 +02:00
* > above solutions were abducted from [[this|https://i.imgur.com/E3En0gJ.png]] and [[this|https://i.imgur.com/lpnTz3A.png]] survey result
2023-05-22 13:56:33 +02:00
*
2023-05-23 12:35:05 +02:00
* !!!Demo
*
* <$videojs controls="controls" aspectratio="16:9" preload="auto" poster="" fluid="fluid" class="vjs-big-play-centered">
* <source src="https://coderofsalvation.github.io/xrfragment.media/href.mp4" type="video/mp4"/>
* </$videojs>
*
* > capture of <a href="./example/aframe/sandbox" target="_blank">aframe/sandbox</a>
2023-05-22 13:56:33 +02:00
*/