UNPKG

@zimjs/three

Version:

Three is a helper module for the ZIM JavaScript Canvas Framework that imports three.js and provides a fast easy way to get started, bring three.js into ZIM or bring ZIM into three.js as TextureActives for full 2D interactivity on any texture / material in

1,142 lines (1,006 loc) 46 kB
// ZIM - https://zimjs.com - Code Creativity! // JavaScript Canvas Framework for General Interactive Media // free to use - donations welcome of course! https://zimjs.com/donate // ZIM THREE - see https://zimjs.com/code#libraries for examples // ~~~~~~~~~~~~~~~~~~~~~~~~ // DESCRIPTION // Three is an add-on ZIM module to help with three.js // The Three Module has Three(), XRControllers(), XRMovement(), and XRTeleport() // DOCS // Docs are provided at https://zimjs.com/docs.html // See THREE MODULE at bottom // ~~~~~~~~~~~~~~~~~~~~~~~~ var WW = window||{}; import zim from "zimjs"; // import * as THREE from "three"; // could use this but will tree-shake import { AdditiveBlending, BoxGeometry, BufferGeometry, CanvasTexture, CircleGeometry, Color, ColorManagement, ConeGeometry, CylinderGeometry, DoubleSide, FrontSide, Group, Line, LinearSRGBColorSpace, LineBasicMaterial, Matrix4, Mesh, MeshBasicMaterial, OrthographicCamera, PerspectiveCamera, PlaneGeometry, Quaternion, Raycaster, RingGeometry, Scene, TorusGeometry, Vector2, Vector3, WebGLRenderer } from "three"; // note - removed the ES5 module pattern as we are getting zim from import // ~~~~~~~~~~~~~~~~~~~~~~~~ zim.threeCanvasNum = 0; zim.Three = function(width, height, color, cameraPosition, cameraLook, interactive, resize, frame, ortho, textureActive, colorSpace, colorManagement, legacyLights, throttle, lay, full, xr, VRButton, xrBufferScale, tag) { var sig = "width, height, color, cameraPosition, cameraLook, interactive, resize, frame, ortho, textureActive, colorSpace, colorManagement, legacyLights, throttle, lay, full, xr, VRButton, xrBufferScale, tag"; var duo; if (duo = zob(zim.Three, arguments, sig, this)) return duo; if (zot(frame)) frame = WW.zdf; if (zot(createjs)) {zog("ZIM THREE needs a createjs namespace active"); return;} if (zot(width)) width = frame.width; if (zot(height)) height = frame.height; if (zot(cameraPosition)) cameraPosition = new Vector3(0, 200, 200); // x, y, z - pulled back and above if (zot(cameraLook)) camera = null; // set to scene positon if (zot(interactive)) interactive = false; // no interaction on three.js if (zot(resize)) resize = true; if (zot(frame)) frame = zimDefaultFrame; if (zot(ortho)) ortho = false; if (zot(textureActive)) textureActive = false; if (zot(colorSpace)) colorSpace = LinearSRGBColorSpace; if (zot(colorManagement)) colorManagement = false; if (zot(legacyLights)) legacyLights = false; if (zot(throttle)) throttle = false; if (textureActive) interactive = true; if (zot(lay)) lay = null; if (zot(full)) full = false; if (zot(xr)) xr = false; if (zot(VRButton)) VRButton = xr?window.VRButton:false; if (zot(xrBufferScale)) xrBufferScale = 2; if (zot(WW.zimDefaultThree)) WW.zimDefaultThree = this; this.frame = frame; var that = this; var pRatio = frame.retina?(window.devicePixelRatio || 1):1; if (tag) { if (tag.clientWidth == null) tag = zid(tag); width = tag.clientWidth?tag.clientWidth:width; height = tag.clientHeight?tag.clientHeight:height; } // RENDERER if (window.WebGLRenderingContext || document.createElement('canvas').getContext('experimental-webgl')) { var renderer = that.renderer = new WebGLRenderer({alpha: true, antialias:true}); renderer.setSize(width*pRatio, height*pRatio); if (ortho) renderer.autoClear = false; renderer.outputColorSpace = colorSpace; ColorManagement.enabled = colorManagement; if (legacyLights) renderer.useLegacyLights = true; this.success = true; } else { this.success = false; return; } // CANVAS var canvas = this.canvas = renderer.domElement; canvas.setAttribute("id", "zimThree" + zim.threeCanvasNum++); // starts at 0 (post assignment incrementor) canvas.setAttribute("width", width*pRatio); canvas.setAttribute("height", height*pRatio); if (tag) canvas.style.position = "relative"; else canvas.style.position = "absolute"; canvas.style.left = "0px"; canvas.style.top = "0px"; canvas.style.width = width+'px'; canvas.style.height = height+'px'; if (lay=="under") { canvas.style.zIndex = -20; frame.color = clear; if (interactive) frame.canvas.style.pointerEvents = "none"; } // frame.canvas.parentNode.insertBefore(canvas, frame.canvas.nextSibling); document.body.appendChild(canvas); if (!interactive) canvas.style.pointerEvents = "none"; if (!textureActive && !full) { // DOM ELEMENT // use this in ZIM to set x, y, scaleX, scaleY, rotation, alpha // and stage.removeChild(DOMElement); // it does not have ZIM 4TH methods // you cannot interact with it - like click, drag, etc. // If you set the Three object's interactive to true // then you can use DOM JavaScript with addEventListener to interact // but then you can't interact with ZIM underneath the DOMElement // see CreateJS Docs for DOMElement for more info: // https://www.createjs.com/docs/easeljs/classes/DOMElement.html var DOMElement = this.DOMElement = new createjs.DOMElement(canvas); DOMElement.setBounds(0,0,width,height); zim.centerReg(DOMElement, null, null, false); frame.stage.addChild(DOMElement); } else { if (tag) { width = tag.clientWidth; height = tag.clientHeight; } else { width = window.innerWidth; height = window.innerHeight; } } // POSITION AND SCALE DOMElement // the DOM Element is window scale not zim Frame scale // so need to be able to convert from window to frame // use this in ZIM to position and scale the Three DOMElement this.position = function(x, y) { if (textureActive || full) return; if (zot(x)) { x = zot(that.realX)?frame.stage.width/2:that.realX; } else { that.realX = x; } if (zot(y)) { y = zot(that.realY)?frame.stage.height/2:that.realY; } else { that.realY = y; } if (frame.retina) { DOMElement.x = frame.x/frame.stage.scaleX + x/pRatio; DOMElement.y = frame.y/frame.stage.scaleY + y/pRatio; } else { DOMElement.x = frame.x + x*frame.scale; DOMElement.y = frame.y + y*frame.scale; } } this.scale = function(s) { if (textureActive || full) return; if (zot(s)) { s = zot(that.realS)?1:that.realS; } else { that.realS = s; } if (frame.retina) { DOMElement.scaleX = DOMElement.scaleY = s/pRatio; } else { DOMElement.scaleX = DOMElement.scaleY = frame.scale*s; } } // SCENE var scene = this.scene = new Scene(); if (!zot(color)) scene.background = new Color(color); // CAMERA // standard settings var viewAngle = 70; var aspect = width/height; var near = 0.1; var far = 200000; var camera = this.camera = new PerspectiveCamera(viewAngle, aspect, near, far); scene.add(camera); camera.position.set(cameraPosition.x,cameraPosition.y,cameraPosition.z); camera.lookAt(zot(cameraLook)?scene.position:cameraLook); if (xr) { renderer.xr.enabled = true; renderer.xr.setFramebufferScaleFactor(xrBufferScale); if (VRButton) { that.vrButton = VRButton.createButton(renderer, scene); document.body.appendChild(that.vrButton); } } if (ortho) { // this does not scale objects - good for HUD // last two parameters are near and far - objects on ortho scene must be placed from 0-10 var cameraOrtho = that.cameraOrtho = new OrthographicCamera(-width/2, width/2, height/2, -height/2, 0, 10); var sceneOrtho = that.sceneOrtho = new Scene(); cameraOrtho.position.z = 10; } if (tag && tag.appendChild) tag.appendChild(renderer.domElement); // RESIZE if (textureActive || full) { if (resize) { that.resizeEvent = function() { var width, height; if (tag) { width = tag.clientWidth; height = tag.clientHeight; } else { width = window.innerWidth; height = window.innerHeight; } camera.aspect = width/height; camera.updateProjectionMatrix(); if (ortho) { cameraOrtho.left = -width/2; cameraOrtho.right = width/2; cameraOrtho.top = height/2; cameraOrtho.bottom = -height/2; cameraOrtho.updateProjectionMatrix(); } renderer.setSize(width, height); } window.addEventListener('resize', that.resizeEvent, false); that.resizeEvent(); } } else { if (resize) { that.resizeEvent = function() { that.scale(); that.position(); }; window.addEventListener('resize', that.resizeEvent, false); that.resizeEvent(); } } setTimeout(function () { if (frame && frame.update) { frame.update(); if (resize) that.resizeEvent(); } }, 20); // RENDERER ENGINE // var request; function render() { // request = requestAnimationFrame(render); if (that.preRender) that.preRender(); if (that.controllerRender) that.controllerRender(); if (that.teleportRender) that.teleportRender(); if (that.movementRender) that.movementRender(); renderer.render(scene,camera); if (that.postRender) that.postRender(); } function renderOrtho() { // request = requestAnimationFrame(renderOrtho); renderer.clear(); if (that.preRender) that.preRender(); if (that.controllerRender) that.controllerRender(); if (that.teleportRender) that.teleportRender(); if (that.movementRender) that.movementRender(); renderer.render(scene,camera); if (that.postRender) that.postRender(); renderer.clearDepth(); renderer.render(sceneOrtho, cameraOrtho); } function renderT() { // request = requestAnimationFrame(renderT); if (++tc%throttle!=0) return; if (that.preRender) that.preRender(); if (that.controllerRender) that.controllerRender(); if (that.teleportRender) that.teleportRender(); if (that.movementRender) that.movementRender(); renderer.render(scene,camera); if (that.postRender) that.postRender(); } function renderOrthoT() { // request = requestAnimationFrame(renderOrthoT); if (++tc%throttle!=0) return; renderer.clear(); if (that.preRender) that.preRender(); if (that.controllerRender) that.controllerRender(); if (that.teleportRender) that.teleportRender(); if (that.movementRender) that.movementRender(); renderer.render(scene,camera); if (that.postRender) that.postRender(); renderer.clearDepth(); renderer.render(sceneOrtho, cameraOrtho); } // if (throttle) { // var tc = 0; // if (ortho) renderOrthoT(); // else renderT(); // } else { // if (ortho) renderOrtho(); // else render(); // } if (throttle) { var tc = 0; if (ortho) renderer.setAnimationLoop(renderOrthoT); else renderer.setAnimationLoop(renderT); } else { if (ortho) renderer.setAnimationLoop(renderOrtho); else renderer.setAnimationLoop(render); } // HELPER METHODS FOR ROTATING AROUND AXIS this.rotateAroundAxis = function(obj, axis, radians) { var rotWorldMatrix = new Matrix4(); rotWorldMatrix.makeRotationAxis(axis.normalize(), radians); rotWorldMatrix.multiply(obj.matrix); obj.matrix = rotWorldMatrix; obj.rotation.setFromRotationMatrix(obj.matrix); } this.rotateAroundObjectAxis = function(obj, axis, radians) { var rotObjectMatrix = new Matrix4(); rotObjectMatrix.makeRotationAxis(axis.normalize(), radians); obj.matrix.multiply(rotObjectMatrix); obj.rotation.setFromRotationMatrix(obj.matrix); } this.makePanel = function(textureActive, textureActives, scale, curve, opacity, material, doubleSide, colorSpace) { var sig = "textureActive, textureActives, scale, curve, opacity, material, doubleSide, colorSpace"; var duo; if (duo = zob(that.makePanel, arguments, sig)) return duo; if (zot(textureActive)) return; if (zot(scale)) scale = .5; if (zot(curve) || curve === true) curve = false; if (zot(opacity)) opacity = 1; if (zot(material) || typeof material == "string") material = MeshBasicMaterial; var geometry = new PlaneGeometry(textureActive.width*scale, textureActive.height*scale, curve?20:1, curve?20:1); if (curve) that.curvePlane(geometry, curve); var texture = new CanvasTexture(textureActive.canvas); if (zot(doubleSide)) doubleSide = false; if (!zot(colorSpace)) texture.colorSpace = colorSpace; var material = new (material)({ map:texture, side:doubleSide?DoubleSide:FrontSide, transparent:true, opacity:opacity }); var mesh = new Mesh(geometry, material); if (textureActives) { var layer; if (textureActives.layers) layer = textureActives.layers[0]; textureActives.addMesh(mesh, layer); } if (!that.positionEvent) { that.positionItems = new zim.Dictionary(true); that.positionEvent = function() { var width = window.innerWidth; var height = window.innerHeight; zim.loop(that.positionItems.values, function(v) { var w = v.obj.geometry.parameters.width*v.obj.scale.x; var h = v.obj.geometry.parameters.height*v.obj.scale.y; if (v.horizontal == "left") { v.obj.position.x = width > v.gutter ? -width/2+(v.x+w/2) : (v.x+w/2)-v.gutter/2; } else if (v.horizontal == "right") { v.obj.position.x = width > v.gutter ? width/2-(v.x+w/2) : -(v.x+w/2)+v.gutter/2; } else { v.obj.position.x = v.x; } if (v.vertical == "top") { v.obj.position.y = height/2 - (v.y+h/2); } else if (v.vertical == "bottom") { v.obj.position.y = -height/2 + (v.y+h/2); } else { v.obj.position.y = v.y; } }); }; window.addEventListener('resize', that.positionEvent, false); } mesh.pos = function(x,y,horizontal,vertical,gutter) { if (zot(x)) x = 0; if (zot(y)) y = 0; if (zot(horizontal)) horizontal = "left"; if (zot(vertical)) vertical = "top"; if (zot(gutter)) gutter = 0; that.positionItems.add(this, {obj:this, x:x, y:y, horizontal:horizontal, vertical:vertical, gutter:gutter}); that.positionEvent(); return this; } return mesh; } this.posMesh = function(mesh,x,y,horizontal,vertical,gutter) { mesh.pos(x,y,horizontal,vertical,gutter); return this; } this.flipMaterial = function(materialType, params) { if (zot(materialType)) materialType = MeshBasicMaterial; if (params.map) { params.map.center = new Vector2(0.5, 0.5); params.map.rotation = Math.PI; params.map.flipY = false; } var m = new (materialType)(params); m.userData.ta_flipped = true; // sent to ZIM TextureActive to adjust RayCast UV x return m; } this.curvePlane = function(geometry, z) { // prisoner849 Paul West three.js board var negative = z<0; z = Math.abs(z); // if (z<0) { // negative = true; // z = Math.abs(z); // } var p = geometry.parameters; var hw = p.width * 0.5; var a = new Vector2(-hw, 0); var b = new Vector2(0, z); var c = new Vector2(hw, 0); var ab = new Vector2().subVectors(a, b); var bc = new Vector2().subVectors(b, c); var ac = new Vector2().subVectors(a, c); var r = (ab.length() * bc.length() * ac.length()) / (2 * Math.abs(ab.cross(ac))); var center = new Vector2(0, z - r); var baseV = new Vector2().subVectors(a, center); var baseAngle = baseV.angle() - (Math.PI * 0.5); var arc = baseAngle * 2; var uv = geometry.attributes.uv; var pos = geometry.attributes.position; var mainV = new Vector2(); for (var i = 0; i < uv.count; i++){ var uvRatio = 1 - uv.getX(i); var y = pos.getY(i); mainV.copy(c).rotateAround(center, (arc * uvRatio)); pos.setXYZ(i, mainV.x, y, negative?mainV.y:-mainV.y); } } this.dispose = function() { // thanks https://discourse.threejs.org/t/when-to-dispose-how-to-completely-clean-up-a-three-js-scene/1549/15 var scene = that.scene; if (DOMElement) frame.stage.removeChild(DOMElement); if (renderer.domElement && renderer.domElement.parentElement) renderer.domElement.parentElement.removeChild(renderer.domElement); if (that.renderer) that.renderer.dispose(); if (scene) { scene.traverse(function(object) { if (!object.isMesh) return; object.geometry.dispose(); if (object.material.isMaterial) { cleanMaterial(object.material); } else { for (var material of object.material) cleanMaterial(material); } }); function cleanMaterial (material) { material.dispose(); for (var key of Object.keys(material)) { var value = material[key] if (value && typeof value === 'object' && 'minFilter' in value) { value.dispose(); } } } } if (that.canvas) that.canvas.style.display = "none"; frame.off("resize", that.resizeEvent); that.renderer = null; that.canvas = null; that.DOMElement = null; that.camera = null; that.cameraOrtho = null; that.scene = null; that.sceneOrtho = null; that.vrButton = null; } } zim.XRControllers = function(three, type, color, highlightColor, lineColor, lineLength, threshhold) { var sig = "three, type, color, highlightColor, lineColor, lineLength, threshhold"; var duo; if (duo = zob(zim.XRControllers, arguments, sig, this)) return duo; this.type = "XRControllers"; if (zot(type)) type = "laser"; if (zot(color)) color = "#cccccc"; if (zot(highlightColor)) highlightColor = ["violet",zim.blue] if (zot(lineColor)) lineColor = "#cccccc"; if (zot(lineLength)) lineLength = 5; if (zot(threshhold)) threshhold = .2; var renderer = three.renderer; var scene = three.scene; if (!Array.isArray(type)) { if (type.isObject3D) { zogy("Three XRControllerSet() - must pass two custom controllers") type = [type, "laser"]; // material should not be cloned due to rollover } else { type = [type, type]; } } if (!Array.isArray(color)) color = [color, color]; if (!Array.isArray(highlightColor)) highlightColor = [highlightColor, highlightColor]; if (highlightColor[0]==-1) highlightColor[0] = color[0]; if (highlightColor[1]==-1) highlightColor[1] = color[1]; if (!Array.isArray(lineColor)) lineColor = [lineColor, lineColor]; if (!Array.isArray(lineLength)) lineLength = [lineLength, lineLength]; var that = this; that.threshhold = threshhold; if (type[0] != -1) this.controller1 = makeC(0); if (type[1] != -1) this.controller2 = makeC(1); three.controllerRender = function() { that.controller1.dispatchController(); that.controller2.dispatchController(); } function makeC(num) { var controller = renderer.xr.getController(num); scene.add(controller); controller.userData.highlights = []; var which = num==0?"left":"right"; controller.userData.hand = which; if (type[num].isObject3D) { var grip = controller.grip = renderer.xr.getControllerGrip(num); scene.add(grip); } else { var holder = controller.holder = new Group(); // grip.add(holder); // holder.rotation.x = 300*RAD; // these work on Chrome but are off on Quest browser // holder.position.y = -.0682; // holder.position.z = -.074; controller.add(holder); } controller.addEventListener("connected", function(e) { controller.gamepad = e.data.gamepad; if (num==0) { that.XR = true; var ev = new createjs.Event("xrconnected"); ev.data = that.data = e.data; that.dispatchEvent(ev); } // RETICULE if (lineColor[num] != -1) { var geometry, material, reticle; if (e.data && e.data.targetRayMode == "tracked-pointer") { geometry = new BufferGeometry(); material = new LineBasicMaterial({color: lineColor[num]}); geometry.setFromPoints([new Vector3(0, 0, 0), new Vector3(0, 0, -lineLength[num])]); reticle = controller.reticle = new Line(geometry, material); } else { geometry = new RingGeometry(.02, .04, 32 ).translate(0, 0, - 1); material = new MeshBasicMaterial({opacity: .5, transparent: true}); reticle = controller.reticle = new Mesh(geometry, material); } controller.add(reticle); reticle.userData.xrteleportignore = true; } }); controller.addEventListener("disconnected", function(e) { if (num==0) { that.XR = false; that.dispatchEvent("xrdisconnected"); } }); var pressed = [0,0,0,0,0,0,0]; var touched = [0,0,0,0,0,0,0]; var axes = [that.threshhold,that.threshhold,that.threshhold,that.threshhold]; controller.dispatchController = function() { var data = controller.gamepad; if (!controller.gamepad) return; for(var i=0; i<pressed.length; i++) { var p = data.buttons[i].pressed?1:0; var t = data.buttons[i].touched?1:0; if (p!=pressed[i]) { var e = new createjs.Event(p?"pressdown":"pressup"); e.controller = controller; e.num = i; e.hand = controller.userData.hand; e.value = data.buttons[i].value that.dispatchEvent(e); if (e.num == 0) { if (e.hand == "left") that.triggerLeft = p; if (e.hand == "right") that.triggerRight = p; } if (e.num == 1) { if (e.hand == "left") that.squeezeLeft = p; if (e.hand == "right") that.squeezeRight = p; } if (e.num == 2) { if (e.hand == "left") that.padLeft = p; if (e.hand == "right") that.padRight = p; } } if (p!=touched[i]) { var e = new createjs.Event(p?"touchstart":"touchend"); e.controller = controller; e.num = i; e.hand = controller.userData.hand; e.value = data.buttons[i].value that.dispatchEvent(e); } pressed[i] = p; touched[i] = t; } for(i=0; i<axes.length; i++) { var a = data.axes[i]; if (Math.abs(a) < that.threshhold) a = that.threshhold; if (a!=axes[i]) { var e = new createjs.Event("axes"); e.controller = controller; e.num = i; e.hand = controller.userData.hand; e.dir = (i==0 || i==2)?"horizontal":"vertical"; e.value = data.axes[i] that.dispatchEvent(e); } axes[i] = a; } } // scene.add(holder); // holder.scale.set(20,20,20) // holder.position.x = num==0?-2:2; if (type[num].isObject3D) { grip.add(type[num]); var hl = type[num].userData.highlights; if (hl && !Array.isArray(hl)) type[num].userData.highlights = [hl]; // transfer highlights controller.userData.highlights = type[num].userData.highlights; } else { if (type[num] != "line") { // BODY - common except for line var handleGeometry = new CylinderGeometry(.009, .009, .09, 30); var handleMaterial = [ new MeshBasicMaterial({color: color[num], transparent:true, opacity:.7}), new MeshBasicMaterial({color: "#666666", transparent:true, opacity:.7}), new MeshBasicMaterial({color: "#ffffff", transparent:true, opacity:.7}) ]; var handle = new Mesh(handleGeometry, handleMaterial); handle.rotation.x = 90*RAD; handle.position.z = .045; holder.add(handle); controller.userData.highlights.push(handle.material[0]); } if (type[num] == "gun" || type[num] == "raygun") { var gunGeometry = new BoxGeometry(.018, .018, .05); var gunMaterial = new MeshBasicMaterial({color: color[num], transparent:true, opacity:.7}); var gun = new Mesh(gunGeometry, gunMaterial); gun.rotation.x = 65*RAD; gun.position.y = -.0155; gun.position.z = .07; holder.add(gun); } if (type[num] == "raygun") { var ringGeometry1 = new TorusGeometry( .015, .004, 16, 100 ); var ringMaterial = new MeshBasicMaterial({color: color[num], transparent:true, opacity:.7}); var ring1 = new Mesh(ringGeometry1, ringMaterial); ring1.position.z = .036; ring1.scale.z = .7; holder.add(ring1); var ringGeometry2 = new TorusGeometry( .018, .004, 16, 100 ); var ring2 = new Mesh(ringGeometry2, ringMaterial); ring2.position.z = .017; ring2.scale.z = .7; holder.add(ring2); } if (type[num] == "sword") { var ringGeometry = new TorusGeometry( .015, .007, 16, 100 ); var ringMaterial = new MeshBasicMaterial({color: color[num], transparent:true, opacity:.7}); var ring = new Mesh(ringGeometry, ringMaterial); ring.position.z = .003; ring.scale.z = .4; holder.add(ring); } if (type[num] == "pen") { var coneGeometry = new ConeGeometry(.009, .02, 10); var coneMaterial = new MeshBasicMaterial({color: white, transparent:true, opacity:.6}); var cone = new Mesh(coneGeometry, coneMaterial); cone.rotation.x = -90*RAD; cone.position.z = .01; holder.add(cone) handle.position.z += .02; } } var last = []; controller.startEvent = controller.addEventListener("selectstart", function(e) { var hl = controller.userData.highlights; if (hl) { for(var i=0; i<hl.length; i++) { last[i] = hl[i].color.getHex(); hl[i].color.set(highlightColor[num]); } } controller.userData.selecting = true; that.dispatchEvent("controller"+controller.userData.hand+"down"); that.dispatchEvent("selectstart"); }); controller.endEvent = controller.addEventListener("selectend", function(e) { var hl = controller.userData.highlights; if (hl) { for(var i=0; i<hl.length; i++) { hl[i].color.set(last[i]); } } controller.userData.selecting = false; that.dispatchEvent("controller"+controller.userData.hand+"up"); that.dispatchEvent("selectend"); }); controller.moveEvent = controller.addEventListener('move', function(e) { that.dispatchEvent("controller"+controller.userData.hand+"move"); that.dispatchEvent("move"); }); return controller; } this.dispose = function() { if (that.controller1) disposeController(that.controller1); if (that.controller2) disposeController(that.controller2); function disposeController(c) { c.removeEventListener('selectstart', c.startEvent); c.removeEventListener('selectend', c.endEvent); c.removeEventListener('move', c.moveEvent); if (c.grip) { c.grip.traverse(function(obj) {obj.dispose?.()}); scene.remove(c.grip); } if (c.holder) { c.holder.traverse(function(obj) {obj.dispose?.()}); scene.remove(c.holder); } scene.remove(c); } } } zim.extend(zim.XRControllers, createjs.EventDispatcher); zim.XRMovement = function(three, XRControllers, speed, acceleration, rotationSpeed, rotationAcceleration, hapticMax, verticalStrafe, radiusMax, threshhold, directionFix, boxMax, rotationAngle, rotationInterval) { var sig = "three, XRControllers, speed, acceleration, rotationSpeed, rotationAcceleration, hapticMax, verticalStrafe, radiusMax, threshhold, directionFix, boxMax, rotationAngle, rotationInterval"; var duo; if (duo = zob(zim.XRMovement, arguments, sig, this)) return duo; this.type = "XRMovement"; if (zot(speed)) speed = 1; if (zot(rotationSpeed)) rotationSpeed = 1; if (zot(acceleration)) acceleration = .3; if (zot(rotationAcceleration)) rotationAcceleration = .2; if (zot(hapticMax)) hapticMax = 0; if (zot(verticalStrafe)) verticalStrafe = false; if (zot(radiusMax)) radiusMax = -1; if (zot(threshhold)) threshhold = .2; if (zot(directionFix)) directionFix = true; if (zot(rotationAngle)) rotationAngle = null; if (zot(rotationInterval)) rotationInterval = .5; var that = this; that.speed = speed; that.rotationSpeed = rotationSpeed; that.acceleration = acceleration; that.rotationAcceleration = rotationAcceleration; that.hapticMax = hapticMax; that.verticalStrafe = verticalStrafe; that.radiusMax = radiusMax; that.threshhold = threshhold; that.boxMax = boxMax; that.rotationAngle = rotationAngle; that.rotationInterval = rotationInterval; var renderer = three.renderer; var scene = three.scene; var camera = three.camera; var xrc = XRControllers; var speedFactor = [0.01, 0.01, 0.01, 0.01]; var rotationFactor = .1; var dolly; var cameraVector = new Vector3(); var dolly = this.dolly = new Group(); dolly.userData.rotationY = 0; dolly.position.set(0, 0, 0); dolly.name = "dolly"; scene.add(dolly); dolly.add(camera); if (xrc.controller1) dolly.add(xrc.controller1); if (xrc.controller2) dolly.add(xrc.controller2); if (xrc.controller1.grip) dolly.add(xrc.controller1.grip); if (xrc.controller2.grip) dolly.add(xrc.controller2.grip); if (directionFix) { var bG = new Mesh(new BoxGeometry(.01,.01,.01), new MeshBasicMaterial()); camera.add(bG); } // split into events and movement and modified from // https://codepen.io/jason-buchheim/pen/zYqYGXM var lastX = 0; var lastY = 0; var lastZ = 0; var rotationCheck = true; var rotationID; xrc.on("axes", function(e) { // THUMBSTICKS or TOUCHPAD var value = e.value; var i = e.num; if (Math.abs(value) > that.threshhold) { // // worked in R149 but was absolute in R155 // // so added a box to the camera and use that for R155 if (directionFix) { // R155 bG.getWorldDirection(cameraVector); } else { var xrCamera = renderer.xr.getCamera(camera); xrCamera.getWorldDirection(cameraVector); } var accel = ((e.hand == "left" && xrc.squeezeLeft) || (e.hand == "right" && xrc.squeezeRight)); var amount = e.value / 11 * (accel?2:1); if (i == 0 || i == 2) { // HORIZONTAL if (e.hand == "left") { // LEFT // move side to side // reverse the vectors 90 degrees so can do straffing side to side movement speedFactor[i] >= that.speed ? (speedFactor[i] = that.speed) : (speedFactor[i] *= (1 + that.acceleration)); dolly.position.x -= -cameraVector.z * speedFactor[i] * amount; dolly.position.z += -cameraVector.x * speedFactor[i] * amount; if (radiusMax >= 0 || that.boxMax) checkMax(); if (hapticMax > 0) that.doHaptic(value, "left", that.hapticMax); } else { // RIGHT if (that.rotationAngle != null) { if (that.rotationAngle == 0) return; if (rotationCheck && Math.abs(value) > .8) { var amount = -that.rotationAngle*zim.RAD*zim.sign(e.value); dolly.rotateY(amount); dolly.userData.rotationY += amount; rotationCheck = false; if (rotationID) clearTimeout(rotationID); rotationID = setTimeout(function(){rotationCheck=true;}, (accel?(that.rotationInterval*.66):that.rotationInterval)*1000); } } else { rotationFactor >= that.rotationSpeed ? (rotationFactor = that.rotationSpeed) : (rotationFactor *= (1 + that.rotationAcceleration)); var rotationAmount = e.value * 1 * rotationFactor * (accel?1.5:1); dolly.rotateY(-rotationAmount*zim.RAD); } } } if (i == 1 || i == 3) { // VERTICAL speedFactor[i] >= that.speed ? (speedFactor[i] = that.speed) : (speedFactor[i] *= (1 + that.acceleration)); if (e.hand == "left") { // LEFT if (that.verticalStrafe) { // strafe up and down dolly.position.y -= speedFactor[i] * e.value; } else { // move forward and backward (default) dolly.position.x -= -cameraVector.x * speedFactor[i] * amount; dolly.position.z -= -cameraVector.z * speedFactor[i] * amount; } if (that.radiusMax >= 0 || that.boxMax) checkMax(); if (that.hapticMax > 0) that.doHaptic(value, "left", that.hapticMax); } else { // RIGHT dolly.position.x -= -cameraVector.x * speedFactor[i] * amount; dolly.position.z -= -cameraVector.z * speedFactor[i] * amount; if (that.radiusMax >= 0 || that.boxMax) checkMax(); if (that.hapticMax > 0) that.doHaptic(value, "right", that.hapticMax); } } } else { if ((i == 0 || i == 2) && e.hand == "right") rotationCheck = true; speedFactor[i] = .01; rotationFactor = .1; } function checkMax() { if (that.boxMax) { dolly.position.x = zim.constrain(dolly.position.x, that.boxMax[0], that.boxMax[1]); dolly.position.y = zim.constrain(dolly.position.y, that.boxMax[2], that.boxMax[3]); dolly.position.z = zim.constrain(dolly.position.z, that.boxMax[4], that.boxMax[5]); if (that.radiusMax >= 0 && dolly.position.length() > that.radiusMax) { dolly.position.x = lastX; dolly.position.y = lastY; dolly.position.z = lastZ; } else { lastX = dolly.position.x; lastY = dolly.position.y; lastZ = dolly.position.z; } } else if (dolly.position.length() > that.radiusMax) { dolly.position.x = lastX; dolly.position.y = lastY; dolly.position.z = lastZ; } else { lastX = dolly.position.x; lastY = dolly.position.y; lastZ = dolly.position.z; } } }); xrc.on("pressdown", function(e) { if (e.num==3 && e.hand=="left") { dolly.position.x = 0; dolly.position.y = 0; dolly.position.z = 0; dolly.rotation.y = 0; } }); that.doHaptic = function(amount, hand, max) { var c = hand=="left"?that.controller1:that.controller2; if ( c.gamepad.hapticActuators && c.gamepad.hapticActuators[0] ) { c.gamepad.hapticActuators[0].pulse( Math.min(Math.abs(amount), max), 100 ); } return that; } } zim.XRTeleport = function(three, XRControllers, XRMovement, floor, offsetHeight, button, hand, markerColor, markerBlend, markerRadius) { var sig = "three, XRControllers, XRMovement, floor, offsetHeight, button, hand, markerColor, markerBlend, markerRadius"; var duo; if (duo = zob(zim.XRTeleport, arguments, sig, this)) return duo; this.type = "XRTeleport"; // modified from // https://github.com/mrdoob/three.js/blob/dev/examples/webxr_vr_teleport.html if (zot(floor)) floor = []; if (!Array.isArray(floor)) floor = [floor]; if (zot(XRControllers)) {if (zon) {zogy("ZIM XRTeleport - must have XRControllers")} return;} if (zot(offsetHeight)) offsetHeight = 0; if (zot(button)) button = (XRControllers.data && XRControllers.data.gamepad && XRControllers.data.gamepad.buttons.length > 4)?5:0; // forward button or trigger if simple setup if (!Array.isArray(button)) button = [button]; if (zot(hand)) hand = "both"; if (zot(markerColor)) markerColor = zim.silver; if (zot(markerBlend)) markerBlend = AdditiveBlending; if (zot(markerRadius)) markerRadius = .25; if (hand != "both" && hand != "left") hand = "right"; var renderer = three.renderer; var scene = three.scene; var xrc = XRControllers; var xrm = XRMovement; var that = this; that.floor = floor; that.offsetHeight = offsetHeight; that.button = button; that.hand = hand; var INTERSECTION; var tempMatrix = new Matrix4(); var marker = that.marker = new Mesh( new CircleGeometry(markerRadius, 32).rotateX(-Math.PI / 2), new MeshBasicMaterial({color:markerColor, blending:markerBlend}) ); marker.position.y = -offsetHeight; marker.userData.xrteleportignore = true; scene.add(marker); var raycaster = that.raycaster = new Raycaster(); if (!xrm) { var baseReferenceSpace; if (xrc.XR) getBase(); else xrc.on("xrconnected", getBase); function getBase() { baseReferenceSpace = renderer.xr.getReferenceSpace(); } } xrc.on("pressdown", function(e) { var goodButton = false; for (var i=0; i<button.length; i++) { if (e.num==button[i]) { goodButton = true; break; } } if ((that.hand=="both"||that.hand==e.hand) && goodButton) { e.controller.userData.xrteleport = true; } }); xrc.on("pressup", function(e) { var goodButton = false; for (var i=0; i<button.length; i++) { if (e.num==button[i]) { goodButton = true; break; } } if ((that.hand=="both"||that.hand==e.hand) && goodButton) { e.controller.userData.xrteleport = false; if (INTERSECTION) { if (xrm) { xrm.dolly.position.x = INTERSECTION.x; xrm.dolly.position.y = INTERSECTION.y+that.offsetHeight; xrm.dolly.position.z = INTERSECTION.z; } else { var offsetPosition = {x: -INTERSECTION.x, y: -INTERSECTION.y, z: -INTERSECTION.z, w: 1}; var offsetRotation = new Quaternion(); var transform = new XRRigidTransform(offsetPosition, offsetRotation); var teleportSpaceOffset = baseReferenceSpace.getOffsetReferenceSpace(transform); renderer.xr.setReferenceSpace(teleportSpaceOffset); } } } }); three.teleportRender = function() { INTERSECTION = undefined; var c1s, c2s; if (hand=="left" || hand=="both") c1s = xrc.controller1&&xrc.controller1.userData.xrteleport; if (hand=="right" || hand=="both") c2s = xrc.controller2&&xrc.controller2.userData.xrteleport; var controller = c1s?XRControllers.controller1:c2s?XRControllers.controller2:null; if (controller) { tempMatrix.identity().extractRotation(controller.matrixWorld); raycaster.ray.origin.setFromMatrixPosition(controller.matrixWorld); raycaster.ray.direction.set(0, 0, -1).applyMatrix4(tempMatrix); var intersects = raycaster.intersectObjects(scene.children); for (var i=0; i<intersects.length; i++) { var obj = intersects[i].object; if (obj.userData.xrteleportignore) continue; var okay = false; for (var f=0; f<floor.length; f++) { if (obj == floor[f]) { okay = true; break; } } if (!okay) break; // hitting something else INTERSECTION = intersects[i].point; } } if (INTERSECTION) { marker.position.x = INTERSECTION.x; marker.position.y = INTERSECTION.y + .01; marker.position.z = INTERSECTION.z; } marker.visible = INTERSECTION !== undefined; } } export const Three = zim.Three; export const XRControllers = zim.XRControllers; export const XRMovement = zim.XRMovement; export const XRTeleport = zim.XRTeleport;