UNPKG

mdx-m3-viewer

Version:

A browser WebGL model viewer. Mainly focused on models of the games Warcraft 3 and Starcraft 2.

313 lines (257 loc) 8.24 kB
import ModelViewer from './viewer'; import Camera from './camera'; import Grid from './grid'; import ModelInstance from './modelinstance'; import BatchedInstance from './batchedinstance'; import TextureMapper from './texturemapper'; import RenderBatch from './renderbatch'; import EmittedObjectUpdater from './emittedobjectupdater'; /** * A scene. * * Every scene has its own list of model instances, and its own camera and viewport. * * In addition, every scene may have its own AudioContext if enableAudio() is called. * If audo is enabled, the AudioContext's listener's location will be updated automatically. * Note that due to browser policies, this may be done only after user interaction with the web page. */ export default class Scene { viewer: ModelViewer; camera: Camera = new Camera(); grid: Grid = new Grid(-100000, -100000, 200000, 200000, 200000, 200000); visibleCells: number = 0; visibleInstances: number = 0; updatedParticles: number = 0; audioEnabled: boolean = false; audioContext: AudioContext | null = null; instances: ModelInstance[] = []; currentInstance: number = 0; batchedInstances: BatchedInstance[] = []; currentBatchedInstance: number = 0; batches: Map<TextureMapper, RenderBatch> = new Map(); emittedObjectUpdater: EmittedObjectUpdater = new EmittedObjectUpdater(); /** * Similar to WebGL's own `alpha` parameter. * * If false, the scene will be cleared before rendering, meaning that scenes behind it won't be visible through it. * * If true, alpha works as usual. */ alpha: boolean = false; constructor(viewer: ModelViewer) { this.viewer = viewer; let canvas = viewer.canvas; // Use the whole canvas, and standard perspective projection values. this.camera.setViewport(0, 0, canvas.width, canvas.height); this.camera.perspective(Math.PI / 4, canvas.width / canvas.height, 8, 10000); } /** * Creates an AudioContext if one wasn't created already, and resumes it if needed. * * The returned promise will resolve to whether it is actually running or not. * * It may stay in suspended state indefinitly until the user interacts with the page, due to browser policies. */ async enableAudio() { if (typeof AudioContext === 'function') { if (!this.audioContext) { this.audioContext = new AudioContext(); } if (this.audioContext.state !== 'suspended') { await this.audioContext.resume(); } this.audioEnabled = this.audioContext.state === 'running'; return this.audioEnabled; } return false; } /** * Suspend the audio context. */ disableAudio() { if (this.audioContext) { this.audioContext.suspend(); } this.audioEnabled = false; } /** * Sets the scene of the given instance. * * Equivalent to instance.setScene(scene). */ addInstance(instance: ModelInstance) { if (instance.scene !== this) { if (instance.scene) { instance.scene.removeInstance(instance); } instance.scene = this; // Only allow instances that are actually ok to be added the scene. if (instance.model.ok) { this.grid.moved(instance); return true; } } return false; } /** * Remove the given instance from this scene. * * Equivalent to ModelInstance.detach(). */ removeInstance(instance: ModelInstance) { if (instance.scene === this) { this.grid.remove(instance); instance.scene = null; return true; } return false; } /** * Clear this scene. */ clear() { // First remove references to this scene stored in the instances. for (let cell of this.grid.cells) { for (let instance of cell.instances) { instance.scene = null; } } // Then remove references to the instances. this.grid.clear(); } /** * Detach this scene from the viewer. * * Equivalent to viewer.removeScene(scene). */ detach() { if (this.viewer) { return this.viewer.removeScene(this); } return false; } addToBatch(instance: BatchedInstance) { let textureMapper = instance.textureMapper; let batches = this.batches; let batch = batches.get(textureMapper); if (!batch) { batch = instance.getBatch(textureMapper); batches.set(textureMapper, batch); } batch.add(instance); } /** * Update this scene. */ update(dt: number) { let camera = this.camera; // Update the camera. camera.update(); // Update the audio context's position if it exists. if (this.audioContext) { let [x, y, z] = camera.location; let [forwardX, forwardY, forwardZ] = camera.directionY; let [upX, upY, upZ] = camera.directionZ; let listener = this.audioContext.listener; listener.setPosition(-x, -y, -z); listener.setOrientation(forwardX, forwardY, forwardZ, upX, upY, upZ); } let frame = this.viewer.frame; let instances = this.instances; let batchedInstances = this.batchedInstances; let currentInstance = 0; let currentBatchedInstance = 0; this.visibleCells = 0; this.visibleInstances = 0; // Update and collect all of the visible instances. for (let cell of this.grid.cells) { if (cell.isVisible(camera)) { this.visibleCells += 1; for (let instance of cell.instances) { if (instance.rendered && instance.cullFrame < frame && instance.isVisible(camera)) { instance.cullFrame = frame; if (instance.updateFrame < frame) { instance.update(dt, this); } if (instance.isBatched()) { batchedInstances[currentBatchedInstance++] = <BatchedInstance>instance; } else { instances[currentInstance++] = instance; } this.visibleInstances += 1; } } } } batchedInstances.length = currentBatchedInstance; instances.length = currentInstance; instances.sort((a, b) => b.depth - a.depth); this.emittedObjectUpdater.update(dt); this.updatedParticles = this.emittedObjectUpdater.alive; } /** * Use the scene's viewport. * * Should be called before `renderOpaque()` and `renderTranslucent()`. * * Called automatically by `render()`. */ startFrame() { let gl = this.viewer.gl; let viewport = this.camera.viewport; // Set the viewport. gl.viewport(viewport[0], viewport[1], viewport[2], viewport[3]); // Allow to render only in the viewport. gl.scissor(viewport[0], viewport[1], viewport[2], viewport[3]); // If this scene doesn't want alpha, clear it. if (!this.alpha) { gl.depthMask(true); gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); } } /** * Render all opaque things in this scene. */ renderOpaque() { // Clear all of the batches. for (let batch of this.batches.values()) { batch.clear(); } // Add all of the batched instances to batches. for (let instance of this.batchedInstances) { this.addToBatch(instance); } // Render all of the batches. for (let batch of this.batches.values()) { batch.render(); } // Render all of the opaque things of non-batched instances. for (let instance of this.instances) { instance.renderOpaque(); } } /** * Renders all translucent things in this scene. */ renderTranslucent() { for (let instance of this.instances) { instance.renderTranslucent(); } } /** * Render this scene. */ render() { this.startFrame(); this.renderOpaque(); this.renderTranslucent(); } /** * Clear all of the emitted objects in this scene. */ clearEmittedObjects() { for (let object of this.emittedObjectUpdater.objects) { object.health = 0; } } }