scenepic/tssrc/Canvas3D.ts

1316 строки
50 KiB
TypeScript

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(),
<vec3>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(),
<vec3>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>>>", "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 = <mat4>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 = <mat4>Misc.Base64ToFloat32Array(params["WorldToCamera"]);
this.v2sMatrix = <mat4>Misc.Base64ToFloat32Array(params["Projection"]);
}
else {
console.warn("The legacy SetCamera command is deprecated.")
var camCenter = <vec3>Misc.Base64ToFloat32Array(params["Center"]);
var camLookAt = <vec3>Misc.Base64ToFloat32Array(params["LookAt"]);
var camUpDir = <vec3>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 = <vec4>Misc.Base64ToFloat32Array(params["BackgroundColor"]);
// Lighting
this.ambientLightColor = <vec3>Misc.Base64ToFloat32Array(params["AmbientLightColor"]);
this.directionalLightColor = <vec3>Misc.Base64ToFloat32Array(params["DirectionalLightColor"]);
this.directionalLightDir = <vec3>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 = <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 = <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 = <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 = <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 = <Mesh>this.allMeshes[meshId];
this.meshBuffers[meshId] = this.GetMeshBuffers(mesh);
}
}
GetCurrentFocusPointInViewSpace() {
var focusPoint = this.currentFocusPoints[this.currentFrameIndex];
var focusPointPosition = <vec3>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 == "<<<GLOBAL>>>")
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 == "<<<GLOBAL>>>") {
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 == "<<<GLOBAL>>>")
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 == "<<<GLOBAL>>>") {
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 == "<<<GLOBAL>>>")
return this.globalOpacity;
if (layerId == null || !(layerId in this.layerSettings) || !("opacity" in this.layerSettings[layerId]))
return 1.0; // Opaque
else
return <number>this.layerSettings[layerId]["opacity"];
}
SetLayerOpacity(layerId: string, opacity: number) {
if (layerId == null)
return;
if (layerId == "<<<GLOBAL>>>") {
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 <number>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 = <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(<vec3>oldFocusPoint.subarray(3));
var newRotation = aaToMat(<vec3>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, <vec3>oldFocusPoint.subarray(0, 3), w2vMatrixOld);
var newFPView = vec3.create();
vec3.transformMat4(newFPView, <vec3>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 = <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 = <WebGLMeshBuffers>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 = <vec3>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, <vec3>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 = <vec3>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;
}
}
}