From 955baeb2f4999d5cfba62f36a29b08685a07ba49 Mon Sep 17 00:00:00 2001 From: Leon van Kammen Date: Mon, 22 May 2023 13:56:33 +0200 Subject: [PATCH] added docs --- src/3rd/three/xrf/href.js | 169 ++++++++++++++++++++++---------------- 1 file changed, 96 insertions(+), 73 deletions(-) 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) + */