import { mat3, mat4, vec3, vec4, quat, vec2 } from "gl-matrix"; import Misc from "./Misc" import { ObjectCache, CanvasBase } from "./CanvasBase" import Mesh from "./Mesh"; import ShaderProgram from "./Shaders"; import WebGLMeshBuffers from "./WebGLMeshBuffers"; import { MeshPicker } from "./MeshPicker"; // Mesh representing a single cube used to show the user where the focus point // Generated by "ts_assets.py" var FocusPointMeshDefinition = { "Color": "eAFjYGiwZwCDBnsACIIBfwMAAAAB", "IndexBufferType": "UInt16", "LineBuffer": "eAEDAAAAAAEAAAAAAg==", "PrimitiveType": "SingleColorMesh", "TriangleBuffer": "eAENxEkCQDAQALDYKYqi/v9Tc0hotIRObzDGo8lskeJktdnlODucLiUubo9XjavPDycwAZkMAAAAAw==", "VertexBuffer": "eAF1kYENACEIA9nsV3OzX+0fBa2lkJDoaVqqZvba6aiR+0dwZx1XOsCHr7NAR/K/Cw+9ji/TOMdGjhmYKx3gM08W6EjuuZjnOzT3p23605ybU67ClQ7k2t4wz8XpHwu/dD4ziz2hGAAAAAY=" }; // Default camera values var DefaultCamera = { "WorldToCamera": mat4.fromValues(1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, -4.0, 1.0), "FoVYDegrees": 45.0 }; var DefaultNearCropDistance = 0.01; var DefaultFarCropDistance = 20.0; var DegreesToRadians = Math.PI / 180.0; // Default shading parameters var DefaultShading = { "BackgroundColor": [0.0, 0.0, 0.0, 1.0], "AmbientLightColor": [0.5, 0.5, 0.5], "DirectionalLightColor": [0.5, 0.5, 0.5], "DirectionalLightDir": [2.0, -1.0, 1.0] }; // Represents an instantiation of a mesh in a frame class MeshInstance { meshId: string; transform: mat4; constructor(meshId: string, transform: mat4) { this.meshId = meshId; this.transform = transform; } } class MeshData { constructor( public mesh: Mesh, public m2wMatrix: mat4, public m2vMatrix: mat4, public buffer: WebGLMeshBuffers, public opacity: number, public filled: boolean, public wireframe: boolean, public viewDistance: number) { } public Render(gl: WebGL2RenderingContext, v2sMatrix: mat4) { // Optionally turn off back-face culling if (this.mesh.doubleSided) gl.disable(gl.CULL_FACE); else gl.enable(gl.CULL_FACE); // Render this.buffer.RenderBuffer(v2sMatrix, this.m2vMatrix, this.opacity, this.filled, this.wireframe); } public ComputeCentroid(): vec3 { let centroid = vec3.create(); const norm = 1.0 / this.mesh.CountVertices(); for (let i = 0, j = 0; i < this.mesh.CountVertices(); i++, j += this.mesh.ElementsPerVertex) { let p = vec3.transformMat4(vec3.create(), this.mesh.vertexBuffer.subarray(j, j + 3), this.m2wMatrix); vec3.scale(p, p, norm) vec3.add(centroid, centroid, p); } if (this.mesh.CountInstances() == 1) { return centroid; } let instanceCentroid = vec3.create(); const instanceNorm = 1.0 / this.mesh.CountInstances(); for (let i = 0, j = 0; i < this.mesh.CountInstances(); i++, j += this.mesh.ElementsPerInstance) { let p = vec3.add(vec3.create(), this.mesh.instanceBuffer.subarray(j, j + 3), instanceCentroid); vec3.scale(p, p, instanceNorm); vec3.add(instanceCentroid, instanceCentroid, p); } return instanceCentroid; } } export default class Canvas3D extends CanvasBase { allMeshes: any = null // Maps from meshIds to mesh objects gl: WebGL2RenderingContext; sp: ShaderProgram; // Dictionary from layerId to layer settings (wireframe, filled, opacity) layerSettings: { [layerId: string]: { [key: string]: number | boolean } } = {}; layerIds: string[] = []; // Shading parameters bgColor: vec4; ambientLightColor: vec3; directionalLightColor: vec3; directionalLightDir: vec3; globalFill = true; globalWireframe = false; globalOpacity = 1.0; // Mesh picker setFocusToPicked: boolean; pickPoint: vec2; meshPicker: MeshPicker; // Frame instances frameInstances: MeshInstance[][] = []; // [frameIndex][mesh instance within frame] // Mesh buffers *for the current frame* meshBuffers: any = {}; // The webgl mesh buffers for the currently selected frame: dictionary from meshId to webGLMeshBuffer // Pointer speeds pointerAltKeyMultiplier = 0.2; pointerRotationSpeed = 0.01; mouseWheelTranslationSpeed = 0.005; keyDownSpeed = 0.1; thumbStickDeadZoneAmount = 0.2; thumbStickRotationSpeed = 0.1; thumbStickTranslationSpeed = 0.01; touchpadAsButtonThreshold = 0.5; buttonTranslationSpeed = 0.01; buttonScaleSpeed = 1.01; // first person mode cameraVelocity = vec3.fromValues(0.0, 0.0, 0.0); cameraRotationalVelocity = vec2.fromValues(0.0, 0.0); dirKeyPresses = { "w": 0, "a": 0, "s": 0, "d": 0, "q": 0, "e": 0 } // Focus points for user interaction: center of rotation and used for locking view globalFocusPoint: Float32Array = new Float32Array([0.0, 0.0, 0.0]); initialFocusPoints: Float32Array[] = []; // Per-frame focus points (initial version - used for reset camera) currentFocusPoints: Float32Array[] = []; // Per-frame focus points (current version - used for display) focusPointMeshBuffer: WebGLMeshBuffers; showFocusPoint: boolean = false; // Copy of last set camera config globalCameraParams: Object = null; frameCameraParams: Object[] = []; // Per-frame cameras onCameraTrack: boolean = false; // Frame layer settings frameLayerSettings: Object[] = []; // Lock view settings lockViewXY = false; lockViewOrientation = false; // Orbit settings orbitCamera = false; lastOrbitTime: Date = null; // View matrices w2vMatrix: mat4; // World to view (i.e. camera) v2sMatrix: mat4; // View to screen (i.e. projection) constructor(canvasId: string, public frameRate: number, public width: number, public height: number, allMeshes: any, objectCache: ObjectCache, public SetStatus: (status: string) => void, public SetWarning: (message: string) => void, public RequestRedraw: () => void, public ReportFrameIdChange: (canvasId: string, frameId: string) => void, public SimulateKeyPress: (canvasId: string, key: string) => void) { // Base class constructor super(canvasId, frameRate, width, height, objectCache, SetStatus, SetWarning, RequestRedraw, ReportFrameIdChange); // Store meshes this.allMeshes = allMeshes; this.dropdown.style.visibility = "visible"; // Init gl this.InitializeWebGL(); window.addEventListener("webglcontextlost", event => { this.TearDownWebGL(); event.preventDefault(); }); window.addEventListener("webglcontextrestored", event => { this.InitializeWebGL(); event.preventDefault(); }); // Set default scene values this.SetCamera(DefaultCamera); this.globalCameraParams = DefaultCamera; this.onCameraTrack = true; this.SetShading(DefaultShading); this.cameraModeDisplay.style.visibility = "visible"; // Create dropdown this.SetLayerSettings({}); // Start render loop this.StartRenderLoop(); } InitializeWebGL() { // Init gl try { // Get gl context this.gl = this.htmlCanvas.getContext("webgl2"); // Add support for uint draw_elements this.gl.getExtension("OES_element_index_uint"); } catch (e) { } if (this.gl == null) { this.SetWarning("Could not init WebGL"); return; } // Create shader program this.sp = new ShaderProgram(this.gl, "defaultVertex", "defaultFragment"); // Create focus point cuboid var focusPointMesh = Mesh.Parse(FocusPointMeshDefinition); this.focusPointMeshBuffer = new WebGLMeshBuffers(this.gl, this.sp, focusPointMesh); // Create picker this.meshPicker = new MeshPicker(this.gl, this.width, this.height); // Recreate buffers this.PrepareBuffers(); } TearDownWebGL() { this.gl = null; this.sp = null; if (this.focusPointMeshBuffer != null) this.focusPointMeshBuffer.Finalize(); this.focusPointMeshBuffer = null; this.PrepareBuffers(); } private updateCameraVelocityValue(keyPlus: string, keyMinus: string, index: number) { if (this.dirKeyPresses[keyPlus] > this.dirKeyPresses[keyMinus]) { this.cameraVelocity[index] = this.keyDownSpeed } else if (this.dirKeyPresses[keyMinus] > 0) { this.cameraVelocity[index] = -this.keyDownSpeed; } else { this.cameraVelocity[index] = 0; } } private updateCameraVelocity() { this.updateCameraVelocityValue("w", "s", 0); this.updateCameraVelocityValue("a", "d", 1); this.updateCameraVelocityValue("q", "e", 2); } HandlePointerMoveWithTwist(point: vec2, twistAngle: number, event: PointerEvent) { const countPointers = this.pointerCoords.size; if (countPointers == 0) return; const old = this.pointerCoords.get(event.pointerId); let delta = vec2.subtract(vec2.create(), point, old); if (event.altKey) { vec2.scale(delta, delta, this.pointerAltKeyMultiplier); } if (this.FirstPerson) { const init = this.initPointerCoords.get(event.pointerId); let diff = vec2.subtract(vec2.create(), point, init); const length = vec2.length(diff); const deadZone = 10; if (length > deadZone) { let scale = Math.min(2, (length - deadZone) / deadZone); vec2.scale(diff, diff, scale / length); } else { diff = vec2.create(); } vec2.scale(diff, diff, this.pointerRotationSpeed); this.SetCameraRotationalVelocity(diff); } else if (countPointers == 2) { // Deal with pinch-zoom const pinchZoom = this.PinchZoom(point, event); // Change in distances var zOld = this.GetCurrentFocusPointInViewSpace()[2]; var zNew = zOld / pinchZoom.distanceRatio; this.TranslateCamera(vec3.fromValues(0.0, 0.0, zNew - zOld)); const focusDelta = this.ComputeFocusPointRelativeViewSpaceTranslation(old, point); this.TranslateCamera(focusDelta); this.RotateCamera(0.0, 0.0, -pinchZoom.angleDelta); } else { // Deal with basic events if (event.ctrlKey) { // Treat as twist of camera this.RotateCamera(0.0, 0.0, twistAngle); } else if (this.showFocusPoint) { // Translate the 3D center of rotation this.SetFocusPointPositionFromPixelCoordinates(point); } else if (event.shiftKey) // Treat as translation of camera { const focusDelta = this.ComputeFocusPointRelativeViewSpaceTranslation(old, point); this.TranslateCamera(focusDelta); } else if (countPointers == 1) // Treat as rotation of camera about center of rotation { vec2.scale(delta, delta, this.pointerRotationSpeed); // NB y and x are deliberately crossed over this.RotateCamera(delta[1], delta[0], 0.0); } } super.HandlePointerMove(point, event); } HandlePointerUp(event: PointerEvent) { this.SetCameraRotationalVelocity(vec2.create()); super.HandlePointerUp(event); } HandleMouseWheel(event: WheelEvent) { let deltaZ = -event.deltaY * this.mouseWheelTranslationSpeed; if (event.altKey) { deltaZ *= this.pointerAltKeyMultiplier; } var delta = vec3.fromValues(0.0, 0.0, deltaZ); this.TranslateCamera(delta); } HandleKeyUp(key: string) { key = key.toLowerCase(); if (this.firstPerson && key in this.dirKeyPresses) { this.dirKeyPresses[key] = 0; this.updateCameraVelocity(); } else { for (let key in this.dirKeyPresses) { this.dirKeyPresses[key] = 0; } } } HandleKeyDown(key: string): [boolean, boolean] { var result = super.HandleKeyDown(key); if (result[0]) return result; // Already handled var handled = true; key = key.toLowerCase(); switch (key) { case "r": this.ResetView(); break; case "l": this.ToggleLockView(true, false); break; case "o": this.ToggleLockView(false, true); break; case "\\": this.ToggleOrbitCamera(); break; case "capslock": this.showFocusPoint = !this.showFocusPoint; break; default: handled = false; break; } if (this.firstPerson && key in this.dirKeyPresses) { this.dirKeyPresses[key] = Date.now(); this.updateCameraVelocity(); } return [handled, false]; } // Adds UI for the control of certain layers SetLayerSettings(layerSettings: any) { // Delete any previous controls while (this.dropdownTable.childElementCount > 2) this.dropdownTable.removeChild(this.dropdownTable.lastChild); this.layerSettings = layerSettings; this.layerIds = [null]; for (let layerId in layerSettings) { this.layerIds.push(layerId); } // Add header row var BorderStyle = "1px solid #cccccc"; var headerRow = document.createElement("tr"); headerRow.style.borderBottom = BorderStyle; for (var headerName of ["Wire", "Fill", "Opacity", "Layer Id"]) { var headerItem = document.createElement("th"); headerItem.className = "scenepic-dropdown-header"; headerItem.innerHTML = headerName; headerRow.appendChild(headerItem); } this.dropdownTable.appendChild(headerRow); // Create row helper function var createRow = (id, label) => { // Create wireframe checkbox var checkboxWireframe = document.createElement("input"); checkboxWireframe.type = "checkbox"; checkboxWireframe.checked = this.ShowLayerWireframe(id); checkboxWireframe.className = "scenepic-table-control"; checkboxWireframe.addEventListener("change", event => { this.SetLayerWireframe(id, checkboxWireframe.checked); this.PrepareBuffers(); event.stopPropagation(); }); // Create fill checkbox var checkboxFill = document.createElement("input"); checkboxFill.type = "checkbox"; checkboxFill.checked = this.ShowLayerFilled(id); checkboxFill.addEventListener("change", event => { this.SetLayerFilled(id, checkboxFill.checked); this.PrepareBuffers(); event.stopPropagation(); }); // Create opacity slider var sliderOpacity = document.createElement("input"); sliderOpacity.type = "range"; sliderOpacity.min = "0"; sliderOpacity.max = "100"; sliderOpacity.value = String(100.0 * this.GetLayerOpacity(id)); sliderOpacity.addEventListener("change", event => { this.SetLayerOpacity(id, Number(sliderOpacity.value) / 100.0); this.PrepareBuffers(); event.stopPropagation(); }) sliderOpacity.style.width = "50px"; sliderOpacity.style.height = "10px"; // Add layer label var labelLayer = document.createElement("label"); labelLayer.appendChild(document.createTextNode(label)); return [checkboxWireframe, checkboxFill, sliderOpacity, labelLayer]; }; var addRow = (rowItems, border) => { // Create table row var tr = document.createElement("tr"); if (border) tr.style.borderTop = BorderStyle; var addControl = (el, className) => { var td = document.createElement("td"); td.appendChild(el); td.className = className; tr.appendChild(td); }; addControl(rowItems[0], "scenepic-table-control"); addControl(rowItems[1], "scenepic-table-control"); addControl(rowItems[2], "scenepic-table-control"); addControl(rowItems[3], "scenepic-table-layerid"); this.dropdownTable.appendChild(tr); } // Add new controls Object.keys(layerSettings).forEach(id => addRow(createRow(id, id), false)); addRow(createRow("<<>>", "Global"), true); } ConfigureUserInterface(command: any) { if ("PointerAltKeyMultiplier" in command) this.pointerAltKeyMultiplier = command["PointerAltKeyMultiplier"]; if ("PointerRotationSpeed" in command) this.pointerRotationSpeed = command["PointerRotationSpeed"]; if ("MouseWheelTranslationSpeed" in command) this.mouseWheelTranslationSpeed = command["MouseWheelTranslationSpeed"]; if ("KeyDownSpeed" in command) this.keyDownSpeed = command["KeyDownSpeed"]; if ("LayerDropdownVisibility" in command) this.dropdown.style.visibility = command["LayerDropdownVisibility"] } ParseFocusPoint(command: any) { var position = Misc.Base64ToFloat32Array(command["Position"]); if ("OrientationAxisAngle" in command) { var focusPoint = new Float32Array(6); focusPoint.set(position, 0); focusPoint.set(Misc.Base64ToFloat32Array(command["OrientationAxisAngle"]), 3); return focusPoint; } else { return position; } } SetGlobalCamera(value: Object) { if (value == null) return; this.globalCameraParams = value; for (var frameIndex = 0; frameIndex < this.frameCameraParams.length; frameIndex++) this.SetPerFrameCamera(frameIndex, value); this.SetCamera(value); } // Execute a single canvas command ExecuteCanvasCommand(command: any) { switch (command["CommandType"]) { case "ConfigureUserInterface": this.ConfigureUserInterface(command); break; case "SetCamera": var value = command["Value"]; this.SetGlobalCamera(value); break; case "SetFocusPoint": var focusPoint = this.ParseFocusPoint(command); this.SetGlobalFocusPoint(focusPoint); break; case "SetShading": var value = command["Value"]; this.SetShading(value); break; case "SetLayerSettings": var layerSettings = Misc.GetDefault(command, "Value", null); if (layerSettings != null) this.SetLayerSettings(layerSettings); break; default: super.ExecuteCanvasCommand(command); break; } } // Execute a single frame command ExecuteFrameCommand(command: any, frameIndex: number) { switch (command["CommandType"]) { case "SetFocusPoint": var focusPoint = this.ParseFocusPoint(command); this.SetPerFrameFocusPoint(frameIndex, focusPoint); break; case "SetCamera": var value = command["Value"]; this.SetPerFrameCamera(frameIndex, value); break; case "AddMesh": var meshId = command["MeshId"]; var transform = Misc.Base64ToFloat32Array(Misc.GetDefault(command, "Transform", mat4.create())); this.AddMesh(frameIndex, meshId, transform); break; case "RemoveMesh": var meshId = command["MeshId"]; this.RemoveMesh(frameIndex, meshId); break; case "SetLayerSettings": var layerSettings = Misc.GetDefault(command, "Value", null); if (layerSettings != null) { this.SetPerFrameLayerSettings(frameIndex, layerSettings); break; } default: super.ExecuteFrameCommand(command, frameIndex); break; } } SetCamera(params: Object) { if (params == null) return; if (params.hasOwnProperty("WorldToCamera")) { this.w2vMatrix = Misc.Base64ToFloat32Array(params["WorldToCamera"]); this.v2sMatrix = Misc.Base64ToFloat32Array(params["Projection"]); } else { console.warn("The legacy SetCamera command is deprecated.") var camCenter = Misc.Base64ToFloat32Array(params["Center"]); var camLookAt = Misc.Base64ToFloat32Array(params["LookAt"]); var camUpDir = Misc.Base64ToFloat32Array(params["UpDir"]); this.w2vMatrix = mat4.create(); this.v2sMatrix = mat4.create(); mat4.lookAt(this.w2vMatrix, camCenter, camLookAt, camUpDir); mat4.perspective(this.v2sMatrix, params["FoVYDegrees"] * DegreesToRadians, this.width / this.height, DefaultNearCropDistance, DefaultFarCropDistance); } } ResetView() { this.onCameraTrack = true; var currentFrame = this.currentFrameIndex; this.SetCamera(this.frameCameraParams[currentFrame]); this.currentFocusPoints[currentFrame] = new Float32Array(this.initialFocusPoints[currentFrame]) } ToggleLockView(translation: boolean, orientation: boolean) { if (translation) this.lockViewXY = !this.lockViewXY; if (orientation) this.lockViewOrientation = !this.lockViewOrientation; } ToggleOrbitCamera() { this.orbitCamera = !this.orbitCamera; this.lastOrbitTime = null; } SetPerFrameFocusPoint(frameIndex: number, focusPoint: Float32Array) { if (focusPoint == null) return; this.initialFocusPoints[frameIndex] = focusPoint; // For reset support this.currentFocusPoints[frameIndex] = new Float32Array(focusPoint); // Copy } SetPerFrameCamera(frameIndex: number, value: Object) { if (value == null) return; this.frameCameraParams[frameIndex] = value; // For reset support } SetPerFrameLayerSettings(frameIndex: number, value: Object) { if (value == null) return; this.frameLayerSettings[frameIndex] = value; } SetGlobalFocusPoint(focusPoint: Float32Array) { if (focusPoint == null) return; this.globalFocusPoint = focusPoint; for (var frameIndex = 0; frameIndex < this.initialFocusPoints.length; frameIndex++) this.SetPerFrameFocusPoint(frameIndex, focusPoint); // Nice side-effect: object is aliased so will stay shared when user moves focus point } SetShading(params: Object) { // Background color this.bgColor = Misc.Base64ToFloat32Array(params["BackgroundColor"]); // Lighting this.ambientLightColor = Misc.Base64ToFloat32Array(params["AmbientLightColor"]); this.directionalLightColor = Misc.Base64ToFloat32Array(params["DirectionalLightColor"]); this.directionalLightDir = Misc.Base64ToFloat32Array(params["DirectionalLightDir"]); vec3.normalize(this.directionalLightDir, this.directionalLightDir); } AllocateFrame() { this.frameInstances.push([]); this.initialFocusPoints.push(this.globalFocusPoint); this.currentFocusPoints.push(this.globalFocusPoint); this.frameCameraParams.push(this.globalCameraParams); } DeallocateFrame(frameIndex: number) { this.frameInstances[frameIndex] = []; } AddMesh(frameIndex: number, meshId: string, meshTransform: mat4) { // Add the mesh instance var instance = new MeshInstance(meshId, meshTransform); var instances = this.frameInstances[frameIndex]; instances.push(instance); let mesh = this.allMeshes[meshId]; if (mesh.layerId != null && !(mesh.layerId in this.layerSettings)) { this.layerSettings[mesh.layerId] = {}; this.SetLayerSettings(this.layerSettings); } // Update display if this is the selected mesh if (this.currentFrameIndex == frameIndex) { if (!(meshId in this.allMeshes)) this.SetWarning("Mesh " + meshId + " does not exist!"); // Rebuild buffers as needed this.PrepareBuffers(); } } RemoveMesh(frameIndex: number, meshId: string) { // Add the mesh id var instances = this.frameInstances[frameIndex]; var meshIndex = 0; var edited = false; while (meshIndex < instances.length) { if (instances[meshIndex].meshId == meshId) { // Remove instances.splice(meshIndex, 1); edited = true; } else { meshIndex++; } } // Update display if this is the selected mesh if (edited && this.currentFrameIndex == frameIndex) this.PrepareBuffers(); } NotifyMeshUpdated(meshId: string) { if (this.frameInstances.length == 0) return; // No frames var currentFrameInstances = this.frameInstances[this.currentFrameIndex]; for (var instance of currentFrameInstances) { if (instance.meshId == meshId) { // Clear up any existing buffers if (meshId in this.meshBuffers && this.meshBuffers[meshId] != null) this.meshBuffers[meshId].Finalize(); // Recreate new buffers using updated mesh data this.meshBuffers[meshId] = this.GetMeshBuffers(this.allMeshes[meshId]); } } } private GetMeshBuffers(mesh: Mesh) { var textureId = mesh.textureId; var textureSrc = null; if (textureId != null) { var textureSrc = this.objectCache.GetObject(textureId); if (textureSrc == null) return null; // Not able to create buffers yet } return new WebGLMeshBuffers(this.gl, this.sp, mesh, textureSrc); } NotifyTextureUpdated(textureId: string) { if (this.frameInstances.length == 0) return; // No frames var currentFrameInstances = this.frameInstances[this.currentFrameIndex]; for (var instance of currentFrameInstances) { var meshId = instance.meshId; var mesh = this.allMeshes[meshId]; if (mesh.textureId == textureId) { if (this.meshBuffers[meshId] != null) this.meshBuffers[meshId].Finalize(); // Clear up existing buffers this.meshBuffers[meshId] = this.GetMeshBuffers(mesh); // Recreate } } } PrepareBuffers(): void { super.PrepareBuffers(); // Get new set of meshIds var meshIds: string[] = []; if (this.gl != null && this.currentFrameIndex < this.frameInstances.length) // Check for e.g. webgl context lost { for (var instance of this.frameInstances[this.currentFrameIndex]) meshIds.push(instance.meshId); } // Get set of meshIds that need turning in to buffers var meshIdsToAdd: string[] = []; for (var meshId of meshIds) { var mesh = this.allMeshes[meshId]; if (mesh != null && this.IsLayerVisible(mesh.layerId) && !(meshId in this.meshBuffers)) // Visible and not already cached { meshIdsToAdd.push(meshId); // Request texture image be cached if (mesh.textureId != null) this.objectCache.AddUser(mesh.textureId); } } // Finalize meshIds not currently used for (var meshId in this.meshBuffers) { var mesh = this.allMeshes[meshId]; if (!this.IsLayerVisible(mesh.layerId) || meshIds.indexOf(meshId) == -1) // Invisible or no longer present { if (meshId in this.meshBuffers) { if (this.meshBuffers[meshId] != null) this.meshBuffers[meshId].Finalize(); delete this.meshBuffers[meshId]; } // Remove user of texture image from cache if (mesh.textureId != null) this.objectCache.RemoveUser(mesh.textureId); } } // Loop over meshes for (var meshId of meshIdsToAdd) { var mesh = this.allMeshes[meshId]; this.meshBuffers[meshId] = this.GetMeshBuffers(mesh); } } GetCurrentFocusPointInViewSpace() { var focusPoint = this.currentFocusPoints[this.currentFrameIndex]; var focusPointPosition = focusPoint.subarray(0, 3); var focusPointView = vec3.create(); vec3.transformMat4(focusPointView, focusPointPosition, this.w2vMatrix); return focusPointView; } ComputeCameraTwist(point: vec2, event: PointerEvent): number { const old = this.pointerCoords.get(event.pointerId); if (old == undefined) { return 0; } // Compute projection of focal point into canvas image var focusPointImage = vec3.create(); vec3.transformMat4(focusPointImage, this.GetCurrentFocusPointInViewSpace(), this.v2sMatrix); const focus = vec2.fromValues((focusPointImage[0] + 1.0) * 0.5 * this.width, (-focusPointImage[1] + 1.0) * 0.5 * this.height); // Compute rotation angle const angleInitial = Math.atan2(old[1] - focus[1], old[0] - focus[0]); const angleNew = Math.atan2(point[1] - focus[1], point[0] - focus[0]); return angleInitial - angleNew; } SetFocusPointPositionFromPixelCoordinates(pixel: vec2) { if (this.lockViewXY || this.lockViewOrientation) return; this.pickPoint = pixel; this.setFocusToPicked = true; } ComputeFocusPointRelativeViewSpaceTranslation(old: vec2, point: vec2) { var focusPointZ = Math.abs(this.GetCurrentFocusPointInViewSpace()[2]); var clientRect = this.htmlCanvas.getBoundingClientRect(); var s2vMatrix = mat4.create(); mat4.invert(s2vMatrix, this.v2sMatrix); var oldScreen = vec3.fromValues(2.0 * (old[0] / clientRect.width - 0.5), 2.0 * (0.5 - old[1] / clientRect.height), 1.0); var oldView = vec3.create(); vec3.transformMat4(oldView, oldScreen, s2vMatrix); vec3.scale(oldView, oldView, focusPointZ / oldView[2]) // Fix z var newScreen = vec3.fromValues(2.0 * (point[0] / clientRect.width - 0.5), 2.0 * (0.5 - point[1] / clientRect.height), 1.0); var newView = vec3.create(); vec3.transformMat4(newView, newScreen, s2vMatrix); vec3.scale(newView, newView, focusPointZ / newView[2]) // Fix z return vec3.fromValues(oldView[0] - newView[0], oldView[1] - newView[1], 0.0); // oldView[2] - newView[2] should be == 0.0 } RotateCamera(rotateAboutX: number, rotateAboutY: number, rotateAboutZ: number) { this.onCameraTrack = false; var delta = vec3.fromValues(rotateAboutX, rotateAboutY, rotateAboutZ); // Compute focus point location in view space var focusPointView = this.GetCurrentFocusPointInViewSpace(); // Compute transformation matrix var transform = mat4.create(); // Translate to be centered on focus point mat4.translate(transform, transform, focusPointView); // 3 rotation mat4.rotateZ(transform, transform, delta[2]); // Rotate Z mat4.rotateY(transform, transform, delta[1]); // Rotate Y mat4.rotateX(transform, transform, delta[0]); // Rotate X // Translate back vec3.negate(focusPointView, focusPointView); mat4.translate(transform, transform, focusPointView); // Apply change to w2v matrix mat4.multiply(this.w2vMatrix, transform, this.w2vMatrix); } SetCameraRotationalVelocity(rotate: vec2) { vec2.copy(this.cameraRotationalVelocity, rotate); } SwivelCamera(rotate: vec2) { this.onCameraTrack = false; let transform = mat4.create(); mat4.rotateY(transform, transform, rotate[0]); mat4.rotateX(transform, transform, rotate[1]); mat4.multiply(this.w2vMatrix, transform, this.w2vMatrix); } MoveCamera(moveForward: number, moveRight: number, moveUp: number) { this.onCameraTrack = false; let transform = mat4.create(); mat4.fromTranslation(transform, [moveRight, moveUp, moveForward]) mat4.multiply(this.w2vMatrix, transform, this.w2vMatrix); } ScaleCamera(factor: number) { this.onCameraTrack = false; // Compute focus point location in view space var focusPointView = this.GetCurrentFocusPointInViewSpace(); // Compute transformation matrix var transform = mat4.create(); // Translate to be centered on focus point mat4.translate(transform, transform, focusPointView); // Apply scale factor mat4.scale(transform, transform, [factor, factor, factor]); // Translate back vec3.negate(focusPointView, focusPointView); mat4.translate(transform, transform, focusPointView); // Apply change to w2v matrix mat4.multiply(this.w2vMatrix, transform, this.w2vMatrix); } TranslateCamera(delta: vec3) { this.onCameraTrack = false; // Compute translation matrix var transform = mat4.create(); mat4.fromTranslation(transform, delta); // Translate mat4.multiply(this.w2vMatrix, transform, this.w2vMatrix); } IsLayerVisible(layerId: string): boolean { var showFilled = this.ShowLayerFilled(layerId) && this.globalFill; var showWireframe = this.ShowLayerWireframe(layerId) || (this.ShowLayerFilled(layerId) && this.globalWireframe); var opacity = this.GetLayerOpacity(layerId) * this.globalOpacity; return (showFilled || showWireframe) && opacity > 0.0; } ToggleLayerFilled(index: number) { if (index >= this.layerIds.length) { return } let layerId = this.layerIds[index]; let filled = this.ShowLayerFilled(layerId); this.SetLayerFilled(layerId, !filled); } ShowLayerFilled(layerId: string): boolean { if (layerId == "<<>>") return this.globalFill; if (layerId == null) return true; if (!(layerId in this.layerSettings)) return true; if ("filled" in this.layerSettings[layerId] && !this.layerSettings[layerId]["filled"]) return false; return true; } SetLayerFilled(layerId: string, filled: boolean) { if (layerId == null) return; if (layerId == "<<>>") { this.globalFill = filled; } else { if (!(layerId in this.layerSettings)) this.layerSettings[layerId] = {}; this.layerSettings[layerId]["filled"] = filled; } // Prepare buffers and force redraw this.PrepareBuffers(); } ShowLayerWireframe(layerId: string): boolean { if (layerId == "<<>>") return this.globalWireframe; if (layerId == null) return false; if (!(layerId in this.layerSettings)) return false; if ("wireframe" in this.layerSettings[layerId] && this.layerSettings[layerId]["wireframe"]) return true; return false; } SetLayerWireframe(layerId: string, wireframe: boolean) { if (layerId == null) return; if (layerId == "<<>>") { this.globalWireframe = wireframe; } else { if (!(layerId in this.layerSettings)) this.layerSettings[layerId] = {}; this.layerSettings[layerId]["wireframe"] = wireframe; } // Prepare buffers and force redraw this.PrepareBuffers(); } GetLayerOpacity(layerId: string) { if (layerId == "<<>>") return this.globalOpacity; if (layerId == null || !(layerId in this.layerSettings) || !("opacity" in this.layerSettings[layerId])) return 1.0; // Opaque else return this.layerSettings[layerId]["opacity"]; } SetLayerOpacity(layerId: string, opacity: number) { if (layerId == null) return; if (layerId == "<<>>") { this.globalOpacity = opacity; } else { if (!(layerId in this.layerSettings)) this.layerSettings[layerId] = {}; this.layerSettings[layerId]["opacity"] = opacity; } // Prepare buffers and force redraw this.PrepareBuffers(); } GetLayerRenderOrder(layerId: string) { if (layerId == null || !(layerId in this.layerSettings) || !("renderOrder" in this.layerSettings[layerId])) return 1e3; // Last layer - bit of a hack else return this.layerSettings[layerId]["renderOrder"]; } AllMeshBuffersReady() { if (this.frameInstances.length > 0) { for (var instance of this.frameInstances[this.currentFrameIndex]) { // Look up mesh var meshId = instance.meshId; var mesh = this.allMeshes[meshId]; // Check mesh layer visibility if (mesh == null || !this.IsLayerVisible(mesh.layerId)) continue; // Check buffer is ready if (this.meshBuffers[meshId] == null) return false; // Don't draw anything until all buffers ready (to prevent possible flicker) } } return true; } // Override base class implementation to deal with focus point lock ShowFrame(frameIndex: number) { frameIndex = Math.min(frameIndex, this.frameIds.length - 1); // Just in case // Get old and new focus points var oldFocusPoint = this.currentFocusPoints[this.currentFrameIndex]; var newFocusPoint = this.currentFocusPoints[frameIndex]; if (this.frameLayerSettings[frameIndex]) { let layerSettings = this.frameLayerSettings[frameIndex]; for (let layerId in layerSettings) { this.SetLayerFilled(layerId, layerSettings[layerId]["filled"]); this.SetLayerWireframe(layerId, layerSettings[layerId]["wireframe"]); this.SetLayerOpacity(layerId, layerSettings[layerId]["opacity"]); if ("renderOrder" in layerSettings) { this.layerSettings[layerId]["renderOrder"] = layerSettings[layerId]["renderOrder"] } this.PrepareBuffers(); } } var w2vMatrixOld = mat4.create(); mat4.copy(w2vMatrixOld, this.w2vMatrix); // Lock camera rotation to focus point? if (this.lockViewOrientation && oldFocusPoint.length == 6 && newFocusPoint.length == 6) // Does the focus point contain an axis angle rotation? { var aaToMat = (axisAngle: vec3) => { var angle = vec3.length(axisAngle); var axis = vec3.create(); vec3.normalize(axis, axisAngle); var rotation = mat4.create(); mat4.fromRotation(rotation, angle, axis); return rotation; } var oldRotation = aaToMat(oldFocusPoint.subarray(3)); var newRotation = aaToMat(newFocusPoint.subarray(3)); // Compute delta var deltaRotation = oldRotation; mat4.invert(newRotation, newRotation); mat4.multiply(deltaRotation, deltaRotation, newRotation); // Apply delta mat4.multiply(this.w2vMatrix, this.w2vMatrix, deltaRotation); } // Lock camera translation to focus point? if (this.lockViewXY || this.lockViewOrientation) { var oldFPView = vec3.create(); vec3.transformMat4(oldFPView, oldFocusPoint.subarray(0, 3), w2vMatrixOld); var newFPView = vec3.create(); vec3.transformMat4(newFPView, newFocusPoint.subarray(0, 3), this.w2vMatrix); // Compute delta var delta = vec3.create(); vec3.subtract(delta, oldFPView, newFPView); // Apply delta var deltaMat = mat4.create(); mat4.fromTranslation(deltaMat, delta); mat4.multiply(this.w2vMatrix, deltaMat, this.w2vMatrix); } // Call base class return super.ShowFrame(frameIndex); } Render() { // Check for gl to be ready if (this.gl == null) return; // Wait until all resources are ready if (!this.AllMeshBuffersReady()) return; if (this.firstPerson) { this.MoveCamera(this.cameraVelocity[0], this.cameraVelocity[1], this.cameraVelocity[2]); if (this.cameraRotationalVelocity[0] != 0 || this.cameraRotationalVelocity[1] != 0) { this.SwivelCamera(this.cameraRotationalVelocity); } } this.gl.clearColor(this.bgColor[0], this.bgColor[1], this.bgColor[2], this.bgColor[3]); this.gl.clear(this.gl.COLOR_BUFFER_BIT | this.gl.DEPTH_BUFFER_BIT); // Applies to whole canvas this.gl.viewport(0, 0, this.htmlCanvas.width, this.htmlCanvas.height); this.RenderViewport(); } // Render scene method with given w2v and v2s matrices RenderViewport() { const gl = this.gl; const v2sMatrix = this.v2sMatrix; if (this.onCameraTrack) { this.SetCamera(this.frameCameraParams[this.currentFrameIndex]) } // Get copy of w2vMatrix var w2vMatrix = mat4.create(); mat4.copy(w2vMatrix, this.w2vMatrix); // Set up rendering parameters gl.useProgram(this.sp.program); gl.enable(gl.DEPTH_TEST); gl.cullFace(gl.BACK); gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA); // Set lighting gl.uniform3fv(this.sp.ambientLightColorPtr, this.ambientLightColor); gl.uniform3fv(this.sp.directionalLightColorPtr, this.directionalLightColor); gl.uniform3fv(this.sp.directionalLightDirPtr, this.directionalLightDir); // Get list of meshes to draw let opaqueMeshData: MeshData[] = []; let transparentMeshData: MeshData[] = []; if (this.frameInstances.length > 0) { var instances = this.frameInstances[this.currentFrameIndex]; for (var meshInstance of instances) // Loop over mesh instances { // Look up mesh var meshId = meshInstance.meshId; var mesh = this.allMeshes[meshId]; if (mesh == null) continue; // Check mesh layer visibility and opacity var filled = this.ShowLayerFilled(mesh.layerId) && this.globalFill; var wireframe = this.ShowLayerWireframe(mesh.layerId) || (this.ShowLayerFilled(mesh.layerId) && this.globalWireframe); var opacity = this.GetLayerOpacity(mesh.layerId) * this.globalOpacity; if (!(filled || wireframe) || opacity == 0.0) continue; // Get buffer var buffer = this.meshBuffers[meshId]; if (buffer == null) continue; // Compute per-mesh transform var m2vMatrix = mat4.create(); if (mesh.cameraSpace) mat4.copy(m2vMatrix, meshInstance.transform); else mat4.multiply(m2vMatrix, w2vMatrix, meshInstance.transform); // Set properties for labels if (mesh.isLabel) { var textureSrc = this.objectCache.GetObject(mesh.textureId); if (textureSrc != null) { buffer.labelWidthNormalized = textureSrc.desiredWidthPixels / this.htmlCanvas.width; buffer.labelHeightNormalized = textureSrc.desiredHeightPixels / this.htmlCanvas.height; buffer.labelTranslateScreenX = textureSrc.proportionTranslateScreenX * buffer.labelWidthNormalized; buffer.labelTranslateScreenY = textureSrc.proportionTranslateScreenY * buffer.labelHeightNormalized; buffer.labelTranslateWorldX = Misc.Sign(textureSrc.proportionTranslateScreenX) * textureSrc.offsetDistance; buffer.labelTranslateWorldY = Misc.Sign(textureSrc.proportionTranslateScreenY) * textureSrc.offsetDistance; } } // Is this transparent or not? if (opacity < 1.0 || (mesh.textureId != null && (mesh.useTextureAlpha || mesh.isLabel))) { // Transform center of mass to view coordinates to get z-ordering var viewCoM = vec3.create(); vec3.transformMat4(viewCoM, mesh.centerOfMass, m2vMatrix); var viewDistance = viewCoM[2]; // Additionally sort using render order which overrides view distance var renderOrder = this.GetLayerRenderOrder(mesh.layerId); viewDistance += renderOrder * 1e4; // 1e4 is hack to force rendering in batches according to render order. Will break down if viewDistance can be larger than 1e4. transparentMeshData.push(new MeshData(mesh, meshInstance.transform, m2vMatrix, buffer, opacity, filled, wireframe, viewDistance)); } else { opaqueMeshData.push(new MeshData(mesh, meshInstance.transform, m2vMatrix, buffer, 1.0, filled, wireframe, 0)); } } } // Draw opaque meshes gl.depthMask(true); gl.disable(gl.BLEND); opaqueMeshData.forEach(data => data.Render(gl, v2sMatrix)); // Draw transparent meshes, sorted by decreasing distance from camera gl.depthMask(false); gl.enable(gl.BLEND); var sortedTransparentMeshData = transparentMeshData.sort((a, b) => a.viewDistance - b.viewDistance); sortedTransparentMeshData.forEach(data => data.Render(gl, v2sMatrix)); // Show focus point if (this.showFocusPoint) { gl.disable(gl.DEPTH_TEST); var focusPoint = this.currentFocusPoints[this.currentFrameIndex]; var focusPointPosition = focusPoint.subarray(0, 3); var focusPointView = vec3.create(); vec3.transformMat4(focusPointView, focusPointPosition, w2vMatrix); var size = 0.02 * Math.abs(focusPointView[2]); mat4.fromRotationTranslationScale(this.focusPointMeshBuffer.m2wMatrix, quat.create(), focusPointPosition, new Float32Array([size, size, size])); this.focusPointMeshBuffer.RenderBuffer(v2sMatrix, w2vMatrix, 1.0, true, false); } // If enabled, orbit the camera if (this.orbitCamera) { // Adjust for any framerate changes var now = new Date(); if (this.lastOrbitTime != null) { var deltaTime = now.getTime() - this.lastOrbitTime.getTime(); this.RotateCamera(0.0, 0.0025 * deltaTime, 0.0); // Rotate about y axis } this.lastOrbitTime = now; } if (this.setFocusToPicked && opaqueMeshData.length > 0) { let buffers: [WebGLMeshBuffers, mat4][] = []; for (let i = 0; i < opaqueMeshData.length; i++) { let meshData = opaqueMeshData[i]; if (meshData.mesh.cameraSpace) { continue; } meshData.buffer.id = i + 1; buffers.push([meshData.buffer, meshData.m2vMatrix]); } const picked = this.meshPicker.Pick(gl, buffers, this.pickPoint, v2sMatrix) if (picked == 0) { var focusPointView = this.GetCurrentFocusPointInViewSpace(); var s2vMatrix = mat4.create(); var v2wMatrix = mat4.create(); mat4.invert(s2vMatrix, this.v2sMatrix); mat4.invert(v2wMatrix, this.w2vMatrix); // Convert from pixel to screen coordinates var clientRect = this.htmlCanvas.getBoundingClientRect(); var screen = vec3.fromValues(2.0 * (this.pickPoint[0] / clientRect.width - 0.5), 2.0 * (0.5 - this.pickPoint[1] / clientRect.height), 1.0); // Convert from screen to view coordinates var view = vec3.create(); vec3.transformMat4(view, screen, s2vMatrix); vec3.scale(view, view, focusPointView[2] / view[2]) // Fix z value to existing focus point z value // Convert from view to world coordinates var focusPoint = this.currentFocusPoints[this.currentFrameIndex]; var focusPointPosition = focusPoint.subarray(0, 3); vec3.transformMat4(focusPointPosition, view, v2wMatrix); } else { const pickedCentroid = opaqueMeshData[picked - 1].ComputeCentroid(); var focusPoint = this.currentFocusPoints[this.currentFrameIndex]; focusPoint[0] = pickedCentroid[0]; focusPoint[1] = pickedCentroid[1]; focusPoint[2] = pickedCentroid[2]; } this.setFocusToPicked = false; } } }