diff --git a/src/3rd/three/xrf/href.js b/src/3rd/three/xrf/href.js
index fbf1c8e..9c76210 100644
--- a/src/3rd/three/xrf/href.js
+++ b/src/3rd/three/xrf/href.js
@@ -1,67 +1,84 @@
+/**
+ * navigation, portals & mutations
+ *
+ * | fragment | type | scope | example value |
+ * |-|-|-|-|
+ * |`href`| (uri) string | 🔒 |`#pos=1,1,0`
`#pos=1,1,0&rot=90,0,0`
`#pos=pyramid`
`#pos=lastvisit\|pyramid`
`://somefile.gltf#pos=1,1,0`
|
+ *
+ * ### 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
- const world = { pos: new THREE.Vector3(), scale: new THREE.Vector3() }
+ 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);
- // convert texture if needed
+ // detect equirectangular image
let texture = mesh.material.map
if( texture && texture.source.data.height == texture.source.data.width/2 ){
- // assume equirectangular image
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
}
- // poor man's equi-portal
- mesh.material = new THREE.ShaderMaterial( {
- side: THREE.DoubleSide,
- uniforms: {
- pano: { value: texture },
- highlight: { 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 highlight;
- 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 = highlight ? 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) => {
- if( mesh.clicked ) return
- mesh.clicked = true
- let portalArea = 1 // 1 meter
+ let portalArea = 1 // 2 meter
const meshWorldPosition = new THREE.Vector3();
meshWorldPosition.setFromMatrixPosition(mesh.matrixWorld);
@@ -72,35 +89,41 @@ xrf.frag.href = function(v, opts){
cameraDirection.multiplyScalar(portalArea); // move away from portal
const newPos = meshWorldPosition.clone().add(cameraDirection);
- const positionInFrontOfPortal = () => {
- camera.position.copy(newPos);
- camera.lookAt(meshWorldPosition);
-
- if( renderer.xr.isPresenting && xrf.baseReferenceSpace ){ // WebXR VR/AR roomscale reposition
- const offsetPosition = { x: -newPos.x, y: 0, z: -newPos.z, w: 1 };
- const offsetRotation = new THREE.Quaternion();
- const transform = new XRRigidTransform( offsetPosition, offsetRotation );
- const teleportSpaceOffset = xrf.baseReferenceSpace.getOffsetReferenceSpace( transform );
- xrf.renderer.xr.setReferenceSpace( teleportSpaceOffset );
- }
-
- }
-
const distance = camera.position.distanceTo(newPos);
- if( renderer.xr.isPresenting && distance > portalArea ) positionInFrontOfPortal()
- else xrf.navigator.to(v.string) // ok let's surf to HREF!
+ if( renderer.xr.isPresenting && distance > portalArea ) return // too far away
- setTimeout( () => mesh.clicked = false, 200 ) // prevent double clicks
+ xrf.navigator.to(v.string) // ok let's surf to HREF!
+
+ xrf.emit('href',{click:true,mesh,xrf:v})
}
- if( !opts.frag.q ){
+ 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', () => mesh.material.uniforms.highlight.value = true )
- mesh.addEventListener('nocollide', () => mesh.material.uniforms.highlight.value = false )
+ mesh.addEventListener('mousemove', selected(true) )
+ mesh.addEventListener('nocollide', selected(false) )
}
- // lazy remove mesh (because we're inside a traverse)
- setTimeout( (mesh) => {
- xrf.interactive.add(mesh)
- }, 20, mesh )
+ // lazy add mesh (because we're inside a recursive traverse)
+ setTimeout( (mesh) => {
+ xrf.interactive.add(mesh)
+ }, 20, mesh )
}
+
+/**
+ * > above was abducted from [this](https://i.imgur.com/E3En0gJ.png) and [this](https://i.imgur.com/lpnTz3A.png) survey result
+ *
+ * [» source example](https://github.com/coderofsalvation/xrfragment/blob/main/src/three/xrf/pos.js)
+ * [» discussion](https://github.com/coderofsalvation/xrfragment/issues/1)
+ */