/** * xeokit-sdk v2.6.78 * Commit: 17c76751873b26ec38b03ae632f1d2003dc7a98a * Built: 2025-06-18T11:33:48.212Z */ /** @private */ class Map$1 { constructor(items, baseId) { this.items = items || []; this._lastUniqueId = (baseId || 0) + 1; } /** * Usage: * * id = myMap.addItem("foo") // ID internally generated * id = myMap.addItem("foo", "bar") // ID is "foo" */ addItem() { let item; if (arguments.length === 2) { const id = arguments[0]; item = arguments[1]; if (this.items[id]) { // Won't happen if given ID is string throw "ID clash: '" + id + "'"; } this.items[id] = item; return id; } else { item = arguments[0] || {}; while (true) { const findId = this._lastUniqueId++; if (!this.items[findId]) { this.items[findId] = item; return findId; } } } } removeItem(id) { const item = this.items[id]; delete this.items[id]; return item; } } const idMap = new Map$1(); /** * Internal data class that represents the state of a menu or a submenu. * @private */ class Menu { constructor(id) { this.id = id; this.parentItem = null; // Set to an Item when this Menu is a submenu this.groups = []; this.menuElement = null; this.shown = false; this.mouseOver = 0; } } /** * Internal data class that represents a group of Items in a Menu. * @private */ class Group { constructor() { this.items = []; } } /** * Internal data class that represents the state of a menu item. * @private */ class Item { constructor(id, getTitle, doAction, getEnabled, getShown) { this.id = id; this.getTitle = getTitle; this.doAction = doAction; this.getEnabled = getEnabled; this.getShown = getShown; this.itemElement = null; this.subMenu = null; this.enabled = true; } } /** * @desc A customizable HTML context menu. * * [](https://xeokit.github.io/xeokit-sdk/examples/index.html#ContextMenu_Canvas_TreeViewPlugin_Custom) * * * [[Run this example](https://xeokit.github.io/xeokit-sdk/examples/index.html#ContextMenu_Canvas_TreeViewPlugin_Custom)] * * ## Overview * * * A pure JavaScript, lightweight context menu * * Dynamically configure menu items * * Dynamically enable or disable items * * Dynamically show or hide items * * Supports cascading sub-menus * * Configure custom style with CSS (see examples above) * * ## Usage * * In the example below we'll create a ````ContextMenu```` that pops up whenever we right-click on an {@link Entity} within * our {@link Scene}. * * First, we'll create the ````ContextMenu````, configuring it with a list of menu items. * * Each item has: * * * a ````title```` for the item, * * a ````doAction()```` callback to fire when the item's title is clicked, * * an optional ````getShown()```` callback that indicates if the item should shown in the menu or not, and * * an optional ````getEnabled()```` callback that indicates if the item should be shown enabled in the menu or not. * *
* * The ````getShown()```` and ````getEnabled()```` callbacks are invoked whenever the menu is shown. * * When an item's ````getShown()```` callback * returns ````true````, then the item is shown. When it returns ````false````, then the item is hidden. An item without * a ````getShown()```` callback is always shown. * * When an item's ````getEnabled()```` callback returns ````true````, then the item is enabled and clickable (as long as it's also shown). When it * returns ````false````, then the item is disabled and cannot be clicked. An item without a ````getEnabled()```` * callback is always enabled and clickable. * * Note how the ````doAction()````, ````getShown()```` and ````getEnabled()```` callbacks accept a ````context```` * object. That must be set on the ````ContextMenu```` before we're able to we show it. The context object can be anything. In this example, * we'll use the context object to provide the callbacks with the Entity that we right-clicked. * * We'll also initially enable the ````ContextMenu````. * * [[Run this example](https://xeokit.github.io/xeokit-sdk/examples/index.html#ContextMenu_Canvas_Custom)] * * ````javascript * const canvasContextMenu = new ContextMenu({ * * enabled: true, * * items: [ * [ * { * title: "Hide Object", * getEnabled: (context) => { * return context.entity.visible; // Can't hide entity if already hidden * }, * doAction: function (context) { * context.entity.visible = false; * } * } * ], * [ * { * title: "Select Object", * getEnabled: (context) => { * return (!context.entity.selected); // Can't select an entity that's already selected * }, * doAction: function (context) { * context.entity.selected = true; * } * } * ], * [ * { * title: "X-Ray Object", * getEnabled: (context) => { * return (!context.entity.xrayed); // Can't X-ray an entity that's already X-rayed * }, * doAction: (context) => { * context.entity.xrayed = true; * } * } * ] * ] * }); * ```` * * Next, we'll make the ````ContextMenu```` appear whenever we right-click on an Entity. Whenever we right-click * on the canvas, we'll attempt to pick the Entity at those mouse coordinates. If we succeed, we'll feed the * Entity into ````ContextMenu```` via the context object, then show the ````ContextMenu````. * * From there, each ````ContextMenu```` item's ````getEnabled()```` callback will be invoked (if provided), to determine if the item should * be enabled. If we click an item, its ````doAction()```` callback will be invoked with our context object. * * Remember that we must set the context on our ````ContextMenu```` before we show it, otherwise it will log an error to the console, * and ignore our attempt to show it. * * ````javascript* * viewer.scene.canvas.canvas.oncontextmenu = (e) => { // Right-clicked on the canvas * * if (!objectContextMenu.enabled) { * return; * } * * var hit = viewer.scene.pick({ // Try to pick an Entity at the coordinates * canvasPos: [e.pageX, e.pageY] * }); * * if (hit) { // Picked an Entity * * objectContextMenu.context = { // Feed entity to ContextMenu * entity: hit.entity * }; * * objectContextMenu.show(e.pageX, e.pageY); // Show the ContextMenu * } * * e.preventDefault(); * }); * ```` * * Note how we only show the ````ContextMenu```` if it's enabled. We can use that mechanism to switch between multiple * ````ContextMenu```` instances depending on what we clicked. * * ## Dynamic Item Titles * * To make an item dynamically regenerate its title text whenever we show the ````ContextMenu````, provide its title with a * ````getTitle()```` callback. The callback will fire each time you show ````ContextMenu````, which will dynamically * set the item title text. * * In the example below, we'll create a simple ````ContextMenu```` that allows us to toggle the selection of an object * via its first item, which changes text depending on whether we are selecting or deselecting the object. * * [[Run an example](https://xeokit.github.io/xeokit-sdk/examples/index.html#ContextMenu_dynamicItemTitles)] * * ````javascript * const canvasContextMenu = new ContextMenu({ * * enabled: true, * * items: [ * [ * { * getTitle: (context) => { * return (!context.entity.selected) ? "Select" : "Undo Select"; * }, * doAction: function (context) { * context.entity.selected = !context.entity.selected; * } * }, * { * title: "Clear Selection", * getEnabled: function (context) { * return (context.viewer.scene.numSelectedObjects > 0); * }, * doAction: function (context) { * context.viewer.scene.setObjectsSelected(context.viewer.scene.selectedObjectIds, false); * } * } * ] * ] * }); * ```` * * ## Sub-menus * * Each menu item can optionally have a sub-menu, which will appear when we hover over the item. * * In the example below, we'll create a much simpler ````ContextMenu```` that has only one item, called "Effects", which * will open a cascading sub-menu whenever we hover over that item. * * Note that our "Effects" item has no ````doAction```` callback, because an item with a sub-menu performs no * action of its own. * * [[Run this example](https://xeokit.github.io/xeokit-sdk/examples/index.html#ContextMenu_subMenus)] * * ````javascript * const canvasContextMenu = new ContextMenu({ * items: [ // Top level items * [ * { * getTitle: (context) => { * return "Effects"; * }, * * items: [ // Sub-menu * [ * { * getTitle: (context) => { * return (!context.entity.visible) ? "Show" : "Hide"; * }, * doAction: function (context) { * context.entity.visible = !context.entity.visible; * } * }, * { * getTitle: (context) => { * return (!context.entity.selected) ? "Select" : "Undo Select"; * }, * doAction: function (context) { * context.entity.selected = !context.entity.selected; * } * }, * { * getTitle: (context) => { * return (!context.entity.highlighted) ? "Highlight" : "Undo Highlight"; * }, * doAction: function (context) { * context.entity.highlighted = !context.entity.highlighted; * } * } * ] * ] * } * ] * ] * }); * ```` */ class ContextMenu { /** * Creates a ````ContextMenu````. * * The ````ContextMenu```` will be initially hidden. * * @param {Object} [cfg] ````ContextMenu```` configuration. * @param {Object} [cfg.items] The context menu items. These can also be dynamically set on {@link ContextMenu#items}. See the class documentation for an example. * @param {Object} [cfg.context] The context, which is passed into the item callbacks. This can also be dynamically set on {@link ContextMenu#context}. This must be set before calling {@link ContextMenu#show}. * @param {Boolean} [cfg.enabled=true] Whether this ````ContextMenu```` is initially enabled. {@link ContextMenu#show} does nothing while this is ````false````. * @param {Boolean} [cfg.hideOnMouseDown=true] Whether this ````ContextMenu```` automatically hides whenever we mouse-down or tap anywhere in the page. * @param {Boolean} [cfg.hideOnAction=true] Whether this ````ContextMenu```` automatically hides after we select a menu item. Se false if we want the menu to remain shown and show any updates to its item titles, after we've selected an item. * @param {Node | undefined} [cfg.parentNode] Optional reference to an existing DOM Node (e.g. ShadowRoot), to which the menu's HTML element will be appended, defaults to ````document.body````. */ constructor(cfg = {}) { this._id = idMap.addItem(); this._context = null; this._enabled = false; // True when the ContextMenu is enabled this._itemsCfg = []; // Items as given as configs this._rootMenu = null; // The root Menu in the tree this._menuList = []; // List of Menus this._menuMap = {}; // Menus mapped to their IDs this._itemList = []; // List of Items this._itemMap = {}; // Items mapped to their IDs this._shown = false; // True when the ContextMenu is visible this._nextId = 0; this._parentNode = cfg.parentNode || document.body; this._offsetParent = (this._parentNode instanceof ShadowRoot) ? this._parentNode.host : this._parentNode; /** * Subscriptions to events fired at this ContextMenu. * @private */ this._eventSubs = {}; if (cfg.hideOnMouseDown !== false) { this._parentNode.addEventListener("mousedown", (event) => { if (!event.target.classList.contains("xeokit-context-menu-item")) { this.hide(); } }); this._parentNode.addEventListener("touchstart", this._canvasTouchStartHandler = (event) => { if (!event.target.classList.contains("xeokit-context-menu-item")) { this.hide(); } }); } if (cfg.items) { this.items = cfg.items; } this._hideOnAction = (cfg.hideOnAction !== false); this.context = cfg.context; this.enabled = cfg.enabled !== false; this.hide(); } /** Subscribes to an event fired at this ````ContextMenu````. @param {String} event The event @param {Function} callback Callback fired on the event */ on(event, callback) { let subs = this._eventSubs[event]; if (!subs) { subs = []; this._eventSubs[event] = subs; } subs.push(callback); } /** Fires an event at this ````ContextMenu````. @param {String} event The event type name @param {Object} value The event parameters */ fire(event, value) { const subs = this._eventSubs[event]; if (subs) { for (let i = 0, len = subs.length; i < len; i++) { subs[i](value); } } } /** * Sets the ````ContextMenu```` items. * * These can be updated dynamically at any time. * * See class documentation for an example. * * @type {Object[]} */ set items(itemsCfg) { this._clear(); this._itemsCfg = itemsCfg || []; this._parseItems(itemsCfg); this._createUI(); } /** * Gets the ````ContextMenu```` items. * * @type {Object[]} */ get items() { return this._itemsCfg; } /** * Sets whether this ````ContextMenu```` is enabled. * * Hides the menu when disabling. * * @type {Boolean} */ set enabled(enabled) { enabled = (!!enabled); if (enabled === this._enabled) { return; } this._enabled = enabled; if (!this._enabled) { this.hide(); } } /** * Gets whether this ````ContextMenu```` is enabled. * * {@link ContextMenu#show} does nothing while this is ````false````. * * @type {Boolean} */ get enabled() { return this._enabled; } /** * Sets the ````ContextMenu```` context. * * The context can be any object that you need to be provides to the callbacks configured on {@link ContextMenu#items}. * * This must be set before calling {@link ContextMenu#show}. * * @type {Object} */ set context(context) { this._context = context; } /** * Gets the ````ContextMenu```` context. * * @type {Object} */ get context() { return this._context; } /** * Shows this ````ContextMenu```` at the given page coordinates. * * Does nothing when {@link ContextMenu#enabled} is ````false````. * * Logs error to console and does nothing if {@link ContextMenu#context} has not been set. * * Fires a "shown" event when shown. * * @param {Number} pageX Page X-coordinate. * @param {Number} pageY Page Y-coordinate. */ show(pageX, pageY) { if (!this._context) { console.error("ContextMenu cannot be shown without a context - set context first"); return; } if (!this._enabled) { return; } if (this._shown) { return; } this._hideAllMenus(); this._updateItemsTitles(); this._updateItemsEnabledStatus(); this._showMenu(this._rootMenu.id, pageX, pageY); this._updateSubMenuInfo(); this._shown = true; this.fire("shown", {}); } /** * Gets whether this ````ContextMenu```` is currently shown or not. * * @returns {Boolean} Whether this ````ContextMenu```` is shown. */ get shown() { return this._shown; } /** * Hides this ````ContextMenu````. * * Fires a "hidden" event when hidden. */ hide() { if (!this._enabled) { return; } if (!this._shown) { return; } this._hideAllMenus(); this._shown = false; this.fire("hidden", {}); } /** * Destroys this ````ContextMenu````. */ destroy() { this._context = null; this._clear(); if (this._id !== null) { idMap.removeItem(this._id); this._id = null; } } _clear() { // Destroys DOM elements, clears menu data for (let i = 0, len = this._menuList.length; i < len; i++) { const menu = this._menuList[i]; const menuElement = menu.menuElement; menuElement.remove(); } this._itemsCfg = []; this._rootMenu = null; this._menuList = []; this._menuMap = {}; this._itemList = []; this._itemMap = {}; } _parseItems(itemsCfg) { // Parses "items" config into menu data const visitItems = (itemsCfg) => { const menuId = this._getNextId(); const menu = new Menu(menuId); for (let i = 0, len = itemsCfg.length; i < len; i++) { const itemsGroupCfg = itemsCfg[i]; const group = new Group(); menu.groups.push(group); for (let j = 0, lenj = itemsGroupCfg.length; j < lenj; j++) { const itemCfg = itemsGroupCfg[j]; const subItemsCfg = itemCfg.items; const hasSubItems = (subItemsCfg && (subItemsCfg.length > 0)); const itemId = this._getNextId(); const getTitle = itemCfg.getTitle || (() => { return (itemCfg.title || ""); }); const doAction = itemCfg.doAction || itemCfg.callback || (() => { }); const getEnabled = itemCfg.getEnabled || (() => { return true; }); const getShown = itemCfg.getShown || (() => { return true; }); const item = new Item(itemId, getTitle, doAction, getEnabled, getShown); item.parentMenu = menu; group.items.push(item); if (hasSubItems) { const subMenu = visitItems(subItemsCfg); item.subMenu = subMenu; subMenu.parentItem = item; } this._itemList.push(item); this._itemMap[item.id] = item; } } this._menuList.push(menu); this._menuMap[menu.id] = menu; return menu; }; this._rootMenu = visitItems(itemsCfg); } _getNextId() { // Returns a unique ID return "ContextMenu_" + this._id + "_" + this._nextId++; // Start ID with alpha chars to make a valid DOM element selector } _createUI() { // Builds DOM elements for the entire menu tree const visitMenu = (menu) => { this._createMenuUI(menu); const groups = menu.groups; for (let i = 0, len = groups.length; i < len; i++) { const group = groups[i]; const groupItems = group.items; for (let j = 0, lenj = groupItems.length; j < lenj; j++) { const item = groupItems[j]; const subMenu = item.subMenu; if (subMenu) { visitMenu(subMenu); } } } }; visitMenu(this._rootMenu); } _createMenuUI(menu) { // Builds DOM elements for a menu const groups = menu.groups; const html = []; const menuElement= document.createElement("div"); menuElement.classList.add("xeokit-context-menu", menu.id); menuElement.style.zIndex = 300000; menuElement.style.position = "absolute"; html.push(''); const htmlString = html.join(""); menuElement.innerHTML = htmlString; this._parentNode.appendChild(menuElement); menu.menuElement = menuElement; menuElement.style["border-radius"] = 4 + "px"; menuElement.style.display = 'none'; menuElement.style["z-index"] = 300000; menuElement.style.background = "white"; menuElement.style.border = "1px solid black"; menuElement.style["box-shadow"] = "0 4px 5px 0 gray"; menuElement.oncontextmenu = (e) => { e.preventDefault(); }; // Bind event handlers const self = this; let lastSubMenu = null; if (groups) { for (let i = 0, len = groups.length; i < len; i++) { const group = groups[i]; const groupItems = group.items; if (groupItems) { for (let j = 0, lenj = groupItems.length; j < lenj; j++) { const item = groupItems[j]; const itemSubMenu = item.subMenu; item.itemElement = this._parentNode.querySelector(`#${item.id}`); if (!item.itemElement) { console.error("ContextMenu item element not found: " + item.id); continue; } item.itemElement.addEventListener("mouseenter", (event) => { event.preventDefault(); const subMenu = item.subMenu; if (!subMenu) { if (lastSubMenu) { self._hideMenu(lastSubMenu.id); lastSubMenu = null; } return; } if (lastSubMenu && (lastSubMenu.id !== subMenu.id)) { self._hideMenu(lastSubMenu.id); lastSubMenu = null; } if (item.enabled === false) { return; } const itemElement = item.itemElement; const subMenuElement = subMenu.menuElement; const itemRect = itemElement.getBoundingClientRect(); subMenuElement.getBoundingClientRect(); const offsetRect = self._offsetParent.getBoundingClientRect(); const subMenuWidth = 200; // TODO const showOnRight = (itemRect.right + subMenuWidth) < offsetRect.right; const showOnLeft = (itemRect.left - subMenuWidth) > offsetRect.left; if(showOnRight) self._showMenu(subMenu.id, itemRect.right + window.scrollX - 5, itemRect.top + window.scrollY - 1); else if (showOnLeft) self._showMenu(subMenu.id, itemRect.left - subMenuWidth + window.scrollX, itemRect.top + window.scrollY - 1); else { const spaceOnLeft = itemRect.left - offsetRect.left, spaceOnRight = offsetRect.right - itemRect.right; if(spaceOnRight > spaceOnLeft) self._showMenu(subMenu.id, itemRect.right - 5 - (subMenuWidth - spaceOnRight), itemRect.top + window.scrollY - 1); else self._showMenu(subMenu.id, itemRect.left - spaceOnLeft, itemRect.top + window.scrollY - 1); } lastSubMenu = subMenu; }); if (!itemSubMenu) { // Item without sub-menu // clicking item fires the item's action callback item.itemElement.addEventListener("click", (event) => { event.preventDefault(); if (!self._context) { return; } if (item.enabled === false) { return; } if (item.doAction) { item.doAction(self._context); } if (this._hideOnAction) { self.hide(); } else { self._updateItemsTitles(); self._updateItemsEnabledStatus(); } }); item.itemElement.addEventListener("mouseup", (event) => { if (event.which !== 3) { return; } event.preventDefault(); if (!self._context) { return; } if (item.enabled === false) { return; } if (item.doAction) { item.doAction(self._context); } if (this._hideOnAction) { self.hide(); } else { self._updateItemsTitles(); self._updateItemsEnabledStatus(); } }); item.itemElement.addEventListener("mouseenter", (event) => { event.preventDefault(); if (item.enabled === false) { return; } if (item.doHover) { item.doHover(self._context); } }); } } } } } } _updateItemsTitles() { // Dynamically updates the title of each Item to the result of Item#getTitle() if (!this._context) { return; } for (let i = 0, len = this._itemList.length; i < len; i++) { const item = this._itemList[i]; const itemElement = item.itemElement; if (!itemElement) { continue; } const getShown = item.getShown; if (!getShown || !getShown(this._context)) { continue; } const title = item.getTitle(this._context); if (item.subMenu) { itemElement.innerText = title; } else { itemElement.innerText = title; } } } _updateItemsEnabledStatus() { // Enables or disables each Item, depending on the result of Item#getEnabled() if (!this._context) { return; } for (let i = 0, len = this._itemList.length; i < len; i++) { const item = this._itemList[i]; const itemElement = item.itemElement; if (!itemElement) { continue; } const getEnabled = item.getEnabled; if (!getEnabled) { continue; } const getShown = item.getShown; if (!getShown) { continue; } const shown = getShown(this._context); item.shown = shown; if (!shown) { itemElement.style.display = "none"; continue; } else { itemElement.style.display = ""; } const enabled = getEnabled(this._context); item.enabled = enabled; if (!enabled) { itemElement.classList.add("disabled"); } else { itemElement.classList.remove("disabled"); } } } _updateSubMenuInfo() { if (!this._context) return; let itemElement, itemRect, subMenuElement, initialStyles, showOnLeft, subMenuWidth; this._itemList.forEach((item) => { if (item.subMenu) { itemElement = item.itemElement; itemRect = itemElement.getBoundingClientRect(); subMenuElement = item.subMenu.menuElement; initialStyles = { visibility: subMenuElement.style.visibility, display: subMenuElement.style.display, }; subMenuElement.style.display = "block"; subMenuElement.style.visibility = "hidden"; subMenuWidth = item.subMenu.menuElement.getBoundingClientRect().width; subMenuElement.style.visibility = initialStyles.visibility; subMenuElement.style.display = initialStyles.display; showOnLeft = ((itemRect.right + subMenuWidth) > window.innerWidth); itemElement.setAttribute("data-submenuposition", showOnLeft ? "left" : "right"); } }); } _showMenu(menuId, pageX, pageY) { // Shows the given menu, at the specified page coordinates const menu = this._menuMap[menuId]; if (!menu) { console.error("Menu not found: " + menuId); return; } if (menu.shown) { return; } const menuElement = menu.menuElement; if (menuElement) { this._showMenuElement(menuElement, pageX, pageY); menu.shown = true; } } _hideMenu(menuId) { // Hides the given menu const menu = this._menuMap[menuId]; if (!menu) { console.error("Menu not found: " + menuId); return; } if (!menu.shown) { return; } const menuElement = menu.menuElement; if (menuElement) { this._hideMenuElement(menuElement); menu.shown = false; } } _hideAllMenus() { for (let i = 0, len = this._menuList.length; i < len; i++) { const menu = this._menuList[i]; this._hideMenu(menu.id); } } _showMenuElement(menuElement, pageX, pageY) { // Shows the given menu element, at the specified page coordinates menuElement.style.display = 'block'; const menuHeight = menuElement.offsetHeight; const menuWidth = menuElement.offsetWidth; const offsetRect = this._offsetParent.getBoundingClientRect(); const bottomContainerBorder = offsetRect.bottom + window.scrollY; const rightContainerBorder = offsetRect.right + window.scrollX; if ((pageY + menuHeight) > bottomContainerBorder) { pageY = bottomContainerBorder- menuHeight; } if ((pageX + menuWidth) > rightContainerBorder) { pageX = rightContainerBorder - menuWidth; } menuElement.style.left = pageX - offsetRect.left - window.scrollX + 'px'; menuElement.style.top = pageY - offsetRect.top - window.scrollY + 'px'; } _hideMenuElement(menuElement) { menuElement.style.display = 'none'; } } /** * A PointerLens shows a magnified view of a {@link Viewer}'s canvas, centered at the position of the * mouse or touch pointer. * * This component is used by {@link DistanceMeasurementsControl} and {@link AngleMeasurementsControl} * to help position the pointer when snap-to-vertex or snap-toedge is enabled. * * [[Run an example](https://xeokit.github.io/xeokit-sdk/examples/measurement/#distance_createWithMouse_snapping)] * * ````JavaScript * * import {Viewer, XKTLoaderPlugin, AngleMeasurementsPlugin, AngleMeasurementsMouseControl, PointerLens} from "../../dist/xeokit-sdk.es.js"; * * const viewer = new Viewer({ * canvasId: "myCanvas", * dtxEnabled: true * }); * * viewer.camera.eye = [-3.93, 2.85, 27.01]; * viewer.camera.look = [4.40, 3.72, 8.89]; * viewer.camera.up = [-0.01, 0.99, 0.039]; * * const xktLoader = new XKTLoaderPlugin(viewer); * * const sceneModel = xktLoader.load({ * id: "myModel", * src: "../../assets/models/xkt/v10/glTF-Embedded/Duplex_A_20110505.glTFEmbedded.xkt", * edges: true * }); * * const angleMeasurements = new AngleMeasurementsPlugin(viewer); * * const angleMeasurementsMouseControl = new AngleMeasurementsMouseControl(angleMeasurements, { * pointerLens : new PointerLens(viewer, { * zoomFactor: 2 * }) * }) * * angleMeasurementsMouseControl.activate(); * ```` */ class PointerLens { /** * Constructs a new PointerLens. * @param viewer The Viewer * @param [cfg] PointerLens configuration. * @param [cfg.active=true] Whether PointerLens is active. The PointerLens can only be shown when this is `true` (default). */ constructor(viewer, cfg={}) { this.viewer = viewer; this.scene = this.viewer.scene; this._lensCursorDiv = document.createElement('div'); this._lensParams = { canvasSize: 300, cursorBorder: 2, cursorSize: 10 }; this._lensCursorDiv.style.borderRadius = "50%"; this._lensCursorDiv.style.width = this._lensParams.cursorSize + "px"; this._lensCursorDiv.style.height = this._lensParams.cursorSize + "px"; this._lensCursorDiv.style.zIndex = "100000"; this._lensCursorDiv.style.position = "absolute"; this._lensCursorDiv.style.pointerEvents = "none"; this._lensContainer = document.createElement('div'); this._lensContainerId = cfg.containerId || 'xeokit-lens'; this._lensContainer.setAttribute("id", this._lensContainerId); this._lensContainer.style.border = "1px solid black"; this._lensContainer.style.background = "white"; // this._lensContainer.style.opacity = "0"; this._lensContainer.style.borderRadius = "50%"; this._lensContainer.style.width = this._lensParams.canvasSize + "px"; this._lensContainer.style.height = this._lensParams.canvasSize + "px"; this._lensContainer.style.zIndex = "15000"; this._lensContainer.style.position = "absolute"; this._lensContainer.style.pointerEvents = "none"; this._lensContainer.style.visibility = "hidden"; this._lensCanvas = document.createElement('canvas'); this._lensCanvas.id = `${this._lensContainerId}-canvas`; // this._lensCanvas.style.background = "darkblue"; this._lensCanvas.style.borderRadius = "50%"; this._lensCanvas.style.width = this._lensParams.canvasSize + "px"; this._lensCanvas.style.height = this._lensParams.canvasSize + "px"; this._lensCanvas.style.zIndex = "15000"; this._lensCanvas.style.pointerEvents = "none"; document.body.appendChild(this._lensContainer); this._lensContainer.appendChild(this._lensCanvas); this._lensContainer.appendChild(this._lensCursorDiv); this._lensCanvasContext = this._lensCanvas.getContext('2d'); this._canvasElement = this.viewer.scene.canvas.canvas; this._canvasPos = null; this._snappedCanvasPos = null; this._lensPosToggle = cfg.lensPosToggle || true; this._lensPosToggleAmount = cfg.lensPosToggleAmount || 85; this._lensPosMarginLeft = cfg.lensPosMarginLeft || 85; this._lensPosMarginTop = cfg.lensPosMarginTop || 25; this._lensContainer.style.marginTop = `${this._lensPosMarginTop}px`; this._lensContainer.style.marginLeft = `${this._lensPosMarginLeft}px`; this._zoomLevel = cfg.zoomLevel || 2; this._active = (cfg.active !== false); this._visible = false; this.snapped = false; this._onViewerRendering = this.viewer.scene.on("rendering", () => { if (this._active && this._visible) { this.update(); } }); } /** * Updates this PointerLens. */ update() { if (!this._active || !this._visible) { return; } if (!this._canvasPos) { return; } const lensRect = this._lensContainer.getBoundingClientRect(); const canvasRect = this._canvasElement.getBoundingClientRect(); const pointerOnLens = this._canvasPos[0] < lensRect.right && this._canvasPos[0] > lensRect.left && this._canvasPos[1] < lensRect.bottom && this._canvasPos[1] > lensRect.top; this._lensContainer.style.marginLeft = `${this._lensPosMarginLeft}px`; if (pointerOnLens) { if (this._lensPosToggle) { this._lensContainer.style.marginTop = `${canvasRect.bottom - canvasRect.top - this._lensCanvas.height - this._lensPosToggleAmount}px`; } else { this._lensContainer.style.marginTop = `${this._lensPosMarginTop}px`; } this._lensPosToggle = !this._lensPosToggle; } this._lensCanvasContext.clearRect(0, 0, this._lensCanvas.width, this._lensCanvas.height); const size = Math.max(this._lensCanvas.width, this._lensCanvas.height) / this._zoomLevel; this._lensCanvasContext.drawImage( this._canvasElement, // source canvas this._canvasPos[0] - size / 2, // source x (zoom center) this._canvasPos[1] - size / 2, // source y (zoom center) size, // source width size, // source height 0, // destination x 0, // destination y this._lensCanvas.width, // destination width this._lensCanvas.height // destination height ); const middle = this._lensParams.canvasSize / 2 - this._lensParams.cursorSize / 2 - this._lensParams.cursorBorder; const deltaX = this._snappedCanvasPos ? (this._snappedCanvasPos[0] - this._canvasPos[0]) : 0; const deltaY = this._snappedCanvasPos ? (this._snappedCanvasPos[1] - this._canvasPos[1]) : 0; this._lensCursorDiv.style.left = `${middle + deltaX * this._zoomLevel}px`; this._lensCursorDiv.style.top = `${middle + deltaY * this._zoomLevel}px`; } /** * Sets the zoom factor for the lens. * * This is `2` by default. * * @param zoomFactor */ set zoomFactor(zoomFactor) { this._zoomFactor = zoomFactor; this.update(); } /** * Gets the zoom factor for the lens. * * This is `2` by default. * * @returns Number */ get zoomFactor() { return this._zoomFactor; } /** * Sets the canvas central position of the lens. * @param canvasPos */ set canvasPos(canvasPos) { this._canvasPos = canvasPos; this.update(); } /** * Gets the canvas central position of the lens. * @returns {Number[]} */ get canvasPos() { return this._canvasPos; } /** * Sets the canvas coordinates of the pointer. * @param snappedCanvasPos */ set snappedCanvasPos(snappedCanvasPos) { this._snappedCanvasPos = snappedCanvasPos; this.update(); } /** * Gets the canvas coordinates of the snapped pointer. * @returns {Number[]} */ get snappedCanvasPos() { return this._snappedCanvasPos; } /** * Sets if the cursor has snapped to anything. * This is set by plugins. * @param snapped * @private */ set snapped(snapped) { this._snapped = snapped; const [ bg, border ] = snapped ? [ "greenyellow", "green" ] : [ "pink", "red" ]; this._lensCursorDiv.style.background = bg; this._lensCursorDiv.style.border = this._lensParams.cursorBorder + "px solid " + border; } /** * Gets if the cursor has snapped to anything. * This is called by plugins. * @returns {Boolean} * @private */ get snapped() { return this._snapped; } _updateActiveVisible() { this._lensContainer.style.visibility = (this._active && this._visible) ? "visible" : "hidden"; this.update(); } /** * Sets if this PointerLens is active. * @param active */ set active(active) { this._active = active; this._updateActiveVisible(); } /** * Gets if this PointerLens is active. * @returns {Boolean} */ get active() { return this._active; } /** * Sets if this PointerLens is visible. * This is set by plugins. * @param visible * @private */ set visible(visible) { this._visible = visible; this._updateActiveVisible(); } /** * Gets if this PointerLens is visible. * This is called by plugins. * @returns {Boolean} * @private */ get visible() { return this._visible; } /** * Destroys this PointerLens. */ destroy() { if (!this._destroyed) { this.viewer.scene.off(this._onViewerRendering); this._lensContainer.removeChild(this._lensCanvas); document.body.removeChild(this._lensContainer); this._destroyed = true; } } } // Some temporary vars to help avoid garbage collection let doublePrecision = true; let FloatArrayType = doublePrecision ? Float64Array : Float32Array; const tempVec3a$P = new FloatArrayType(3); const tempMat1 = new FloatArrayType(16); const tempMat2 = new FloatArrayType(16); const tempVec4$2 = new FloatArrayType(4); /** * @private */ const math = { setDoublePrecisionEnabled(enable) { doublePrecision = enable; FloatArrayType = doublePrecision ? Float64Array : Float32Array; }, getDoublePrecisionEnabled() { return doublePrecision; }, MIN_DOUBLE: -Number.MAX_SAFE_INTEGER, MAX_DOUBLE: Number.MAX_SAFE_INTEGER, MAX_INT: 10000000, /** * The number of radiians in a degree (0.0174532925). * @property DEGTORAD * @type {Number} */ DEGTORAD: 0.0174532925, /** * The number of degrees in a radian. * @property RADTODEG * @type {Number} */ RADTODEG: 57.295779513, unglobalizeObjectId(modelId, globalId) { const idx = globalId.indexOf("#"); return (idx === modelId.length && globalId.startsWith(modelId)) ? globalId.substring(idx + 1) : globalId; }, globalizeObjectId(modelId, objectId) { return (modelId + "#" + objectId) }, /** * Returns: * - x != 0 => 1/x, * - x == 1 => 1 * * @param {number} x */ safeInv(x) { const retVal = 1 / x; if (isNaN(retVal) || !isFinite(retVal)) { return 1; } return retVal; }, /** * Returns a new, uninitialized two-element vector. * @method vec2 * @param [values] Initial values. * @static * @returns {Number[]} */ vec2(values) { return new FloatArrayType(values || 2); }, /** * Returns a new, uninitialized three-element vector. * @method vec3 * @param [values] Initial values. * @static * @returns {Number[]} */ vec3(values) { return new FloatArrayType(values || 3); }, /** * Returns a new, uninitialized four-element vector. * @method vec4 * @param [values] Initial values. * @static * @returns {Number[]} */ vec4(values) { return new FloatArrayType(values || 4); }, /** * Returns a new, uninitialized 3x3 matrix. * @method mat3 * @param [values] Initial values. * @static * @returns {Number[]} */ mat3(values) { return new FloatArrayType(values || 9); }, /** * Converts a 3x3 matrix to 4x4 * @method mat3ToMat4 * @param mat3 3x3 matrix. * @param mat4 4x4 matrix * @static * @returns {Number[]} */ mat3ToMat4(mat3, mat4 = new FloatArrayType(16)) { mat4[0] = mat3[0]; mat4[1] = mat3[1]; mat4[2] = mat3[2]; mat4[3] = 0; mat4[4] = mat3[3]; mat4[5] = mat3[4]; mat4[6] = mat3[5]; mat4[7] = 0; mat4[8] = mat3[6]; mat4[9] = mat3[7]; mat4[10] = mat3[8]; mat4[11] = 0; mat4[12] = 0; mat4[13] = 0; mat4[14] = 0; mat4[15] = 1; return mat4; }, /** * Returns a new, uninitialized 4x4 matrix. * @method mat4 * @param [values] Initial values. * @static * @returns {Number[]} */ mat4(values) { return new FloatArrayType(values || 16); }, /** * Converts a 4x4 matrix to 3x3 * @method mat4ToMat3 * @param mat4 4x4 matrix. * @param mat3 3x3 matrix * @static * @returns {Number[]} */ mat4ToMat3(mat4, mat3 = new FloatArrayType(9)) { mat3[0] = mat4[0]; mat3[1] = mat4[1]; mat3[2] = mat4[2]; mat3[3] = mat4[4]; mat3[4] = mat4[5]; mat3[5] = mat4[6]; mat3[6] = mat4[8]; mat3[7] = mat4[9]; mat3[8] = mat4[10]; return mat3; }, /** * Converts a list of double-precision values to a list of high-part floats and a list of low-part floats. * @param doubleVals * @param floatValsHigh * @param floatValsLow */ doublesToFloats(doubleVals, floatValsHigh, floatValsLow) { const floatPair = new FloatArrayType(2); for (let i = 0, len = doubleVals.length; i < len; i++) { math.splitDouble(doubleVals[i], floatPair); floatValsHigh[i] = floatPair[0]; floatValsLow[i] = floatPair[1]; } }, /** * Splits a double value into two floats. * @param value * @param floatPair */ splitDouble(value, floatPair) { const hi = FloatArrayType.from([value])[0]; const low = value - hi; floatPair[0] = hi; floatPair[1] = low; }, /** * Returns a new UUID. * @method createUUID * @static * @return string The new UUID */ createUUID: ((() => { const lut = []; for (let i = 0; i < 256; i++) { lut[i] = (i < 16 ? '0' : '') + (i).toString(16); } return () => { const d0 = Math.random() * 0xffffffff | 0; const d1 = Math.random() * 0xffffffff | 0; const d2 = Math.random() * 0xffffffff | 0; const d3 = Math.random() * 0xffffffff | 0; return `${lut[d0 & 0xff] + lut[d0 >> 8 & 0xff] + lut[d0 >> 16 & 0xff] + lut[d0 >> 24 & 0xff]}-${lut[d1 & 0xff]}${lut[d1 >> 8 & 0xff]}-${lut[d1 >> 16 & 0x0f | 0x40]}${lut[d1 >> 24 & 0xff]}-${lut[d2 & 0x3f | 0x80]}${lut[d2 >> 8 & 0xff]}-${lut[d2 >> 16 & 0xff]}${lut[d2 >> 24 & 0xff]}${lut[d3 & 0xff]}${lut[d3 >> 8 & 0xff]}${lut[d3 >> 16 & 0xff]}${lut[d3 >> 24 & 0xff]}`; }; }))(), /** * Clamps a value to the given range. * @param {Number} value Value to clamp. * @param {Number} min Lower bound. * @param {Number} max Upper bound. * @returns {Number} Clamped result. */ clamp(value, min, max) { return Math.max(min, Math.min(max, value)); }, /** * Floating-point modulus * @method fmod * @static * @param {Number} a * @param {Number} b * @returns {*} */ fmod(a, b) { if (a < b) { console.error("math.fmod : Attempting to find modulus within negative range - would be infinite loop - ignoring"); return a; } while (b <= a) { a -= b; } return a; }, /** * Returns true if the two 3-element vectors are the same. * @param v1 * @param v2 * @returns {Boolean} */ compareVec3(v1, v2) { return (v1[0] === v2[0] && v1[1] === v2[1] && v1[2] === v2[2]); }, /** * Returns true if the two 4-element vectors are the same. * @param v1 * @param v2 * @returns {Boolean} */ compareVec4(v1, v2) { return (v1[0] === v2[0] && v1[1] === v2[1] && v1[2] === v2[2] && v1[3] === v2[3]); }, /** * Negates a three-element vector. * @method negateVec3 * @static * @param {Array(Number)} v Vector to negate * @param {Array(Number)} [dest] Destination vector * @return {Array(Number)} dest if specified, v otherwise */ negateVec3(v, dest) { if (!dest) { dest = v; } dest[0] = -v[0]; dest[1] = -v[1]; dest[2] = -v[2]; return dest; }, /** * Negates a four-element vector. * @method negateVec4 * @static * @param {Array(Number)} v Vector to negate * @param {Array(Number)} [dest] Destination vector * @return {Array(Number)} dest if specified, v otherwise */ negateVec4(v, dest) { if (!dest) { dest = v; } dest[0] = -v[0]; dest[1] = -v[1]; dest[2] = -v[2]; dest[3] = -v[3]; return dest; }, /** * Adds one four-element vector to another. * @method addVec4 * @static * @param {Array(Number)} u First vector * @param {Array(Number)} v Second vector * @param {Array(Number)} [dest] Destination vector * @return {Array(Number)} dest if specified, u otherwise */ addVec4(u, v, dest) { if (!dest) { dest = u; } dest[0] = u[0] + v[0]; dest[1] = u[1] + v[1]; dest[2] = u[2] + v[2]; dest[3] = u[3] + v[3]; return dest; }, /** * Adds a scalar value to each element of a four-element vector. * @method addVec4Scalar * @static * @param {Array(Number)} v The vector * @param {Number} s The scalar * @param {Array(Number)} [dest] Destination vector * @return {Array(Number)} dest if specified, v otherwise */ addVec4Scalar(v, s, dest) { if (!dest) { dest = v; } dest[0] = v[0] + s; dest[1] = v[1] + s; dest[2] = v[2] + s; dest[3] = v[3] + s; return dest; }, /** * Adds one three-element vector to another. * @method addVec3 * @static * @param {Array(Number)} u First vector * @param {Array(Number)} v Second vector * @param {Array(Number)} [dest] Destination vector * @return {Array(Number)} dest if specified, u otherwise */ addVec3(u, v, dest) { if (!dest) { dest = u; } dest[0] = u[0] + v[0]; dest[1] = u[1] + v[1]; dest[2] = u[2] + v[2]; return dest; }, /** * Adds a scalar value to each element of a three-element vector. * @method addVec4Scalar * @static * @param {Array(Number)} v The vector * @param {Number} s The scalar * @param {Array(Number)} [dest] Destination vector * @return {Array(Number)} dest if specified, v otherwise */ addVec3Scalar(v, s, dest) { if (!dest) { dest = v; } dest[0] = v[0] + s; dest[1] = v[1] + s; dest[2] = v[2] + s; return dest; }, /** * Subtracts one four-element vector from another. * @method subVec4 * @static * @param {Array(Number)} u First vector * @param {Array(Number)} v Vector to subtract * @param {Array(Number)} [dest] Destination vector * @return {Array(Number)} dest if specified, u otherwise */ subVec4(u, v, dest) { if (!dest) { dest = u; } dest[0] = u[0] - v[0]; dest[1] = u[1] - v[1]; dest[2] = u[2] - v[2]; dest[3] = u[3] - v[3]; return dest; }, /** * Subtracts one three-element vector from another. * @method subVec3 * @static * @param {Array(Number)} u First vector * @param {Array(Number)} v Vector to subtract * @param {Array(Number)} [dest] Destination vector * @return {Array(Number)} dest if specified, u otherwise */ subVec3(u, v, dest) { if (!dest) { dest = u; } dest[0] = u[0] - v[0]; dest[1] = u[1] - v[1]; dest[2] = u[2] - v[2]; return dest; }, /** * Subtracts one two-element vector from another. * @method subVec2 * @static * @param {Array(Number)} u First vector * @param {Array(Number)} v Vector to subtract * @param {Array(Number)} [dest] Destination vector * @return {Array(Number)} dest if specified, u otherwise */ subVec2(u, v, dest) { if (!dest) { dest = u; } dest[0] = u[0] - v[0]; dest[1] = u[1] - v[1]; return dest; }, /** * Get the geometric mean of the vectors. * @method geometricMeanVec2 * @static * @param {...Array(Number)} vectors Vec2 to mean * @return {Array(Number)} The geometric mean vec2 */ geometricMeanVec2(...vectors) { const geometricMean = new FloatArrayType(vectors[0]); for (let i = 1; i < vectors.length; i++) { geometricMean[0] += vectors[i][0]; geometricMean[1] += vectors[i][1]; } geometricMean[0] /= vectors.length; geometricMean[1] /= vectors.length; return geometricMean; }, /** * Subtracts a scalar value from each element of a four-element vector. * @method subVec4Scalar * @static * @param {Array(Number)} v The vector * @param {Number} s The scalar * @param {Array(Number)} [dest] Destination vector * @return {Array(Number)} dest if specified, v otherwise */ subVec4Scalar(v, s, dest) { if (!dest) { dest = v; } dest[0] = v[0] - s; dest[1] = v[1] - s; dest[2] = v[2] - s; dest[3] = v[3] - s; return dest; }, /** * Sets each element of a 4-element vector to a scalar value minus the value of that element. * @method subScalarVec4 * @static * @param {Array(Number)} v The vector * @param {Number} s The scalar * @param {Array(Number)} [dest] Destination vector * @return {Array(Number)} dest if specified, v otherwise */ subScalarVec4(v, s, dest) { if (!dest) { dest = v; } dest[0] = s - v[0]; dest[1] = s - v[1]; dest[2] = s - v[2]; dest[3] = s - v[3]; return dest; }, /** * Multiplies one three-element vector by another. * @method mulVec3 * @static * @param {Array(Number)} u First vector * @param {Array(Number)} v Second vector * @param {Array(Number)} [dest] Destination vector * @return {Array(Number)} dest if specified, u otherwise */ mulVec4(u, v, dest) { if (!dest) { dest = u; } dest[0] = u[0] * v[0]; dest[1] = u[1] * v[1]; dest[2] = u[2] * v[2]; dest[3] = u[3] * v[3]; return dest; }, /** * Multiplies each element of a four-element vector by a scalar. * @method mulVec34calar * @static * @param {Array(Number)} v The vector * @param {Number} s The scalar * @param {Array(Number)} [dest] Destination vector * @return {Array(Number)} dest if specified, v otherwise */ mulVec4Scalar(v, s, dest) { if (!dest) { dest = v; } dest[0] = v[0] * s; dest[1] = v[1] * s; dest[2] = v[2] * s; dest[3] = v[3] * s; return dest; }, /** * Multiplies each element of a three-element vector by a scalar. * @method mulVec3Scalar * @static * @param {Array(Number)} v The vector * @param {Number} s The scalar * @param {Array(Number)} [dest] Destination vector * @return {Array(Number)} dest if specified, v otherwise */ mulVec3Scalar(v, s, dest) { if (!dest) { dest = v; } dest[0] = v[0] * s; dest[1] = v[1] * s; dest[2] = v[2] * s; return dest; }, /** * Multiplies each element of a two-element vector by a scalar. * @method mulVec2Scalar * @static * @param {Array(Number)} v The vector * @param {Number} s The scalar * @param {Array(Number)} [dest] Destination vector * @return {Array(Number)} dest if specified, v otherwise */ mulVec2Scalar(v, s, dest) { if (!dest) { dest = v; } dest[0] = v[0] * s; dest[1] = v[1] * s; return dest; }, /** * Divides one three-element vector by another. * @method divVec3 * @static * @param {Array(Number)} u First vector * @param {Array(Number)} v Second vector * @param {Array(Number)} [dest] Destination vector * @return {Array(Number)} dest if specified, u otherwise */ divVec3(u, v, dest) { if (!dest) { dest = u; } dest[0] = u[0] / v[0]; dest[1] = u[1] / v[1]; dest[2] = u[2] / v[2]; return dest; }, /** * Divides one four-element vector by another. * @method divVec4 * @static * @param {Array(Number)} u First vector * @param {Array(Number)} v Second vector * @param {Array(Number)} [dest] Destination vector * @return {Array(Number)} dest if specified, u otherwise */ divVec4(u, v, dest) { if (!dest) { dest = u; } dest[0] = u[0] / v[0]; dest[1] = u[1] / v[1]; dest[2] = u[2] / v[2]; dest[3] = u[3] / v[3]; return dest; }, /** * Divides a scalar by a three-element vector, returning a new vector. * @method divScalarVec3 * @static * @param v vec3 * @param s scalar * @param dest vec3 - optional destination * @return [] dest if specified, v otherwise */ divScalarVec3(s, v, dest) { if (!dest) { dest = v; } dest[0] = s / v[0]; dest[1] = s / v[1]; dest[2] = s / v[2]; return dest; }, /** * Divides a three-element vector by a scalar. * @method divVec3Scalar * @static * @param v vec3 * @param s scalar * @param dest vec3 - optional destination * @return [] dest if specified, v otherwise */ divVec3Scalar(v, s, dest) { if (!dest) { dest = v; } dest[0] = v[0] / s; dest[1] = v[1] / s; dest[2] = v[2] / s; return dest; }, /** * Divides a four-element vector by a scalar. * @method divVec4Scalar * @static * @param v vec4 * @param s scalar * @param dest vec4 - optional destination * @return [] dest if specified, v otherwise */ divVec4Scalar(v, s, dest) { if (!dest) { dest = v; } dest[0] = v[0] / s; dest[1] = v[1] / s; dest[2] = v[2] / s; dest[3] = v[3] / s; return dest; }, /** * Divides a scalar by a four-element vector, returning a new vector. * @method divScalarVec4 * @static * @param s scalar * @param v vec4 * @param dest vec4 - optional destination * @return [] dest if specified, v otherwise */ divScalarVec4(s, v, dest) { if (!dest) { dest = v; } dest[0] = s / v[0]; dest[1] = s / v[1]; dest[2] = s / v[2]; dest[3] = s / v[3]; return dest; }, /** * Returns the dot product of two four-element vectors. * @method dotVec4 * @static * @param {Array(Number)} u First vector * @param {Array(Number)} v Second vector * @return The dot product */ dotVec4(u, v) { return (u[0] * v[0] + u[1] * v[1] + u[2] * v[2] + u[3] * v[3]); }, /** * Returns the cross product of two four-element vectors. * @method cross3Vec4 * @static * @param {Array(Number)} u First vector * @param {Array(Number)} v Second vector * @return The cross product */ cross3Vec4(u, v) { const u0 = u[0]; const u1 = u[1]; const u2 = u[2]; const v0 = v[0]; const v1 = v[1]; const v2 = v[2]; return [ u1 * v2 - u2 * v1, u2 * v0 - u0 * v2, u0 * v1 - u1 * v0, 0.0]; }, /** * Returns the cross product of two three-element vectors. * @method cross3Vec3 * @static * @param {Array(Number)} u First vector * @param {Array(Number)} v Second vector * @return The cross product */ cross3Vec3(u, v, dest) { if (!dest) { dest = u; } const x = u[0]; const y = u[1]; const z = u[2]; const x2 = v[0]; const y2 = v[1]; const z2 = v[2]; dest[0] = y * z2 - z * y2; dest[1] = z * x2 - x * z2; dest[2] = x * y2 - y * x2; return dest; }, sqLenVec4(v) { // TODO return math.dotVec4(v, v); }, /** * Returns the length of a four-element vector. * @method lenVec4 * @static * @param {Array(Number)} v The vector * @return The length */ lenVec4(v) { return Math.sqrt(math.sqLenVec4(v)); }, /** * Returns the dot product of two three-element vectors. * @method dotVec3 * @static * @param {Array(Number)} u First vector * @param {Array(Number)} v Second vector * @return The dot product */ dotVec3(u, v) { return (u[0] * v[0] + u[1] * v[1] + u[2] * v[2]); }, /** * Returns the dot product of two two-element vectors. * @method dotVec4 * @static * @param {Array(Number)} u First vector * @param {Array(Number)} v Second vector * @return The dot product */ dotVec2(u, v) { return (u[0] * v[0] + u[1] * v[1]); }, sqLenVec3(v) { return math.dotVec3(v, v); }, sqLenVec2(v) { return math.dotVec2(v, v); }, /** * Returns the length of a three-element vector. * @method lenVec3 * @static * @param {Array(Number)} v The vector * @return The length */ lenVec3(v) { return Math.sqrt(math.sqLenVec3(v)); }, distVec3: ((() => { const vec = new FloatArrayType(3); return (v, w) => math.lenVec3(math.subVec3(v, w, vec)); }))(), /** * Returns the length of a two-element vector. * @method lenVec2 * @static * @param {Array(Number)} v The vector * @return The length */ lenVec2(v) { return Math.sqrt(math.sqLenVec2(v)); }, distVec2: ((() => { const vec = new FloatArrayType(2); return (v, w) => math.lenVec2(math.subVec2(v, w, vec)); }))(), /** * @method rcpVec3 * @static * @param v vec3 * @param dest vec3 - optional destination * @return [] dest if specified, v otherwise * */ rcpVec3(v, dest) { return math.divScalarVec3(1.0, v, dest); }, /** * Normalizes a four-element vector * @method normalizeVec4 * @static * @param v vec4 * @param dest vec4 - optional destination * @return [] dest if specified, v otherwise * */ normalizeVec4(v, dest) { const f = 1.0 / math.lenVec4(v); return math.mulVec4Scalar(v, f, dest); }, /** * Normalizes a three-element vector * @method normalizeVec4 * @static */ normalizeVec3(v, dest) { const f = 1.0 / math.lenVec3(v); return math.mulVec3Scalar(v, f, dest); }, /** * Normalizes a two-element vector * @method normalizeVec2 * @static */ normalizeVec2(v, dest) { const f = 1.0 / math.lenVec2(v); return math.mulVec2Scalar(v, f, dest); }, /** * Gets the angle between two vectors * @method angleVec3 * @param v * @param w * @returns {number} */ angleVec3(v, w) { let theta = math.dotVec3(v, w) / (Math.sqrt(math.sqLenVec3(v) * math.sqLenVec3(w))); theta = theta < -1 ? -1 : (theta > 1 ? 1 : theta); // Clamp to handle numerical problems return Math.acos(theta); }, /** * Creates a three-element vector from the rotation part of a sixteen-element matrix. * @param m * @param dest */ vec3FromMat4Scale: ((() => { const tempVec3 = new FloatArrayType(3); return (m, dest) => { tempVec3[0] = m[0]; tempVec3[1] = m[1]; tempVec3[2] = m[2]; dest[0] = math.lenVec3(tempVec3); tempVec3[0] = m[4]; tempVec3[1] = m[5]; tempVec3[2] = m[6]; dest[1] = math.lenVec3(tempVec3); tempVec3[0] = m[8]; tempVec3[1] = m[9]; tempVec3[2] = m[10]; dest[2] = math.lenVec3(tempVec3); return dest; }; }))(), /** * Converts an n-element vector to a JSON-serializable * array with values rounded to two decimal places. */ vecToArray: ((() => { function trunc(v) { return Math.round(v * 100000) / 100000 } return v => { v = Array.prototype.slice.call(v); for (let i = 0, len = v.length; i < len; i++) { v[i] = trunc(v[i]); } return v; }; }))(), /** * Converts a 3-element vector from an array to an object of the form ````{x:999, y:999, z:999}````. * @param arr * @returns {{x: *, y: *, z: *}} */ xyzArrayToObject(arr) { return {"x": arr[0], "y": arr[1], "z": arr[2]}; }, /** * Converts a 3-element vector object of the form ````{x:999, y:999, z:999}```` to an array. * @param xyz * @param [arry] * @returns {*[]} */ xyzObjectToArray(xyz, arry) { arry = arry || math.vec3(); arry[0] = xyz.x; arry[1] = xyz.y; arry[2] = xyz.z; return arry; }, /** * Duplicates a 4x4 identity matrix. * @method dupMat4 * @static */ dupMat4(m) { return m.slice(0, 16); }, /** * Extracts a 3x3 matrix from a 4x4 matrix. * @method mat4To3 * @static */ mat4To3(m) { return [ m[0], m[1], m[2], m[4], m[5], m[6], m[8], m[9], m[10] ]; }, /** * Returns a 4x4 matrix with each element set to the given scalar value. * @method m4s * @static */ m4s(s) { return [ s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s ]; }, /** * Returns a 4x4 matrix with each element set to zero. * @method setMat4ToZeroes * @static */ setMat4ToZeroes() { return math.m4s(0.0); }, /** * Returns a 4x4 matrix with each element set to 1.0. * @method setMat4ToOnes * @static */ setMat4ToOnes() { return math.m4s(1.0); }, /** * Returns a 4x4 matrix with each element set to 1.0. * @method setMat4ToOnes * @static */ diagonalMat4v(v) { return new FloatArrayType([ v[0], 0.0, 0.0, 0.0, 0.0, v[1], 0.0, 0.0, 0.0, 0.0, v[2], 0.0, 0.0, 0.0, 0.0, v[3] ]); }, /** * Returns a 4x4 matrix with diagonal elements set to the given vector. * @method diagonalMat4c * @static */ diagonalMat4c(x, y, z, w) { return math.diagonalMat4v([x, y, z, w]); }, /** * Returns a 4x4 matrix with diagonal elements set to the given scalar. * @method diagonalMat4s * @static */ diagonalMat4s(s) { return math.diagonalMat4c(s, s, s, s); }, /** * Returns a 4x4 identity matrix. * @method identityMat4 * @static */ identityMat4(mat = new FloatArrayType(16)) { mat[0] = 1.0; mat[1] = 0.0; mat[2] = 0.0; mat[3] = 0.0; mat[4] = 0.0; mat[5] = 1.0; mat[6] = 0.0; mat[7] = 0.0; mat[8] = 0.0; mat[9] = 0.0; mat[10] = 1.0; mat[11] = 0.0; mat[12] = 0.0; mat[13] = 0.0; mat[14] = 0.0; mat[15] = 1.0; return mat; }, /** * Returns a 3x3 identity matrix. * @method identityMat3 * @static */ identityMat3(mat = new FloatArrayType(9)) { mat[0] = 1.0; mat[1] = 0.0; mat[2] = 0.0; mat[3] = 0.0; mat[4] = 1.0; mat[5] = 0.0; mat[6] = 0.0; mat[7] = 0.0; mat[8] = 1.0; return mat; }, /** * Tests if the given 4x4 matrix is the identity matrix. * @method isIdentityMat4 * @static */ isIdentityMat4(m) { if (m[0] !== 1.0 || m[1] !== 0.0 || m[2] !== 0.0 || m[3] !== 0.0 || m[4] !== 0.0 || m[5] !== 1.0 || m[6] !== 0.0 || m[7] !== 0.0 || m[8] !== 0.0 || m[9] !== 0.0 || m[10] !== 1.0 || m[11] !== 0.0 || m[12] !== 0.0 || m[13] !== 0.0 || m[14] !== 0.0 || m[15] !== 1.0) { return false; } return true; }, /** * Negates the given 4x4 matrix. * @method negateMat4 * @static */ negateMat4(m, dest) { if (!dest) { dest = m; } dest[0] = -m[0]; dest[1] = -m[1]; dest[2] = -m[2]; dest[3] = -m[3]; dest[4] = -m[4]; dest[5] = -m[5]; dest[6] = -m[6]; dest[7] = -m[7]; dest[8] = -m[8]; dest[9] = -m[9]; dest[10] = -m[10]; dest[11] = -m[11]; dest[12] = -m[12]; dest[13] = -m[13]; dest[14] = -m[14]; dest[15] = -m[15]; return dest; }, /** * Adds the given 4x4 matrices together. * @method addMat4 * @static */ addMat4(a, b, dest) { if (!dest) { dest = a; } dest[0] = a[0] + b[0]; dest[1] = a[1] + b[1]; dest[2] = a[2] + b[2]; dest[3] = a[3] + b[3]; dest[4] = a[4] + b[4]; dest[5] = a[5] + b[5]; dest[6] = a[6] + b[6]; dest[7] = a[7] + b[7]; dest[8] = a[8] + b[8]; dest[9] = a[9] + b[9]; dest[10] = a[10] + b[10]; dest[11] = a[11] + b[11]; dest[12] = a[12] + b[12]; dest[13] = a[13] + b[13]; dest[14] = a[14] + b[14]; dest[15] = a[15] + b[15]; return dest; }, /** * Adds the given scalar to each element of the given 4x4 matrix. * @method addMat4Scalar * @static */ addMat4Scalar(m, s, dest) { if (!dest) { dest = m; } dest[0] = m[0] + s; dest[1] = m[1] + s; dest[2] = m[2] + s; dest[3] = m[3] + s; dest[4] = m[4] + s; dest[5] = m[5] + s; dest[6] = m[6] + s; dest[7] = m[7] + s; dest[8] = m[8] + s; dest[9] = m[9] + s; dest[10] = m[10] + s; dest[11] = m[11] + s; dest[12] = m[12] + s; dest[13] = m[13] + s; dest[14] = m[14] + s; dest[15] = m[15] + s; return dest; }, /** * Adds the given scalar to each element of the given 4x4 matrix. * @method addScalarMat4 * @static */ addScalarMat4(s, m, dest) { return math.addMat4Scalar(m, s, dest); }, /** * Subtracts the second 4x4 matrix from the first. * @method subMat4 * @static */ subMat4(a, b, dest) { if (!dest) { dest = a; } dest[0] = a[0] - b[0]; dest[1] = a[1] - b[1]; dest[2] = a[2] - b[2]; dest[3] = a[3] - b[3]; dest[4] = a[4] - b[4]; dest[5] = a[5] - b[5]; dest[6] = a[6] - b[6]; dest[7] = a[7] - b[7]; dest[8] = a[8] - b[8]; dest[9] = a[9] - b[9]; dest[10] = a[10] - b[10]; dest[11] = a[11] - b[11]; dest[12] = a[12] - b[12]; dest[13] = a[13] - b[13]; dest[14] = a[14] - b[14]; dest[15] = a[15] - b[15]; return dest; }, /** * Subtracts the given scalar from each element of the given 4x4 matrix. * @method subMat4Scalar * @static */ subMat4Scalar(m, s, dest) { if (!dest) { dest = m; } dest[0] = m[0] - s; dest[1] = m[1] - s; dest[2] = m[2] - s; dest[3] = m[3] - s; dest[4] = m[4] - s; dest[5] = m[5] - s; dest[6] = m[6] - s; dest[7] = m[7] - s; dest[8] = m[8] - s; dest[9] = m[9] - s; dest[10] = m[10] - s; dest[11] = m[11] - s; dest[12] = m[12] - s; dest[13] = m[13] - s; dest[14] = m[14] - s; dest[15] = m[15] - s; return dest; }, /** * Subtracts the given scalar from each element of the given 4x4 matrix. * @method subScalarMat4 * @static */ subScalarMat4(s, m, dest) { if (!dest) { dest = m; } dest[0] = s - m[0]; dest[1] = s - m[1]; dest[2] = s - m[2]; dest[3] = s - m[3]; dest[4] = s - m[4]; dest[5] = s - m[5]; dest[6] = s - m[6]; dest[7] = s - m[7]; dest[8] = s - m[8]; dest[9] = s - m[9]; dest[10] = s - m[10]; dest[11] = s - m[11]; dest[12] = s - m[12]; dest[13] = s - m[13]; dest[14] = s - m[14]; dest[15] = s - m[15]; return dest; }, /** * Multiplies the two given 4x4 matrix by each other. * @method mulMat4 * @static */ mulMat4(a, b, dest) { if (!dest) { dest = a; } // Cache the matrix values (makes for huge speed increases!) const a00 = a[0]; const a01 = a[1]; const a02 = a[2]; const a03 = a[3]; const a10 = a[4]; const a11 = a[5]; const a12 = a[6]; const a13 = a[7]; const a20 = a[8]; const a21 = a[9]; const a22 = a[10]; const a23 = a[11]; const a30 = a[12]; const a31 = a[13]; const a32 = a[14]; const a33 = a[15]; const b00 = b[0]; const b01 = b[1]; const b02 = b[2]; const b03 = b[3]; const b10 = b[4]; const b11 = b[5]; const b12 = b[6]; const b13 = b[7]; const b20 = b[8]; const b21 = b[9]; const b22 = b[10]; const b23 = b[11]; const b30 = b[12]; const b31 = b[13]; const b32 = b[14]; const b33 = b[15]; dest[0] = b00 * a00 + b01 * a10 + b02 * a20 + b03 * a30; dest[1] = b00 * a01 + b01 * a11 + b02 * a21 + b03 * a31; dest[2] = b00 * a02 + b01 * a12 + b02 * a22 + b03 * a32; dest[3] = b00 * a03 + b01 * a13 + b02 * a23 + b03 * a33; dest[4] = b10 * a00 + b11 * a10 + b12 * a20 + b13 * a30; dest[5] = b10 * a01 + b11 * a11 + b12 * a21 + b13 * a31; dest[6] = b10 * a02 + b11 * a12 + b12 * a22 + b13 * a32; dest[7] = b10 * a03 + b11 * a13 + b12 * a23 + b13 * a33; dest[8] = b20 * a00 + b21 * a10 + b22 * a20 + b23 * a30; dest[9] = b20 * a01 + b21 * a11 + b22 * a21 + b23 * a31; dest[10] = b20 * a02 + b21 * a12 + b22 * a22 + b23 * a32; dest[11] = b20 * a03 + b21 * a13 + b22 * a23 + b23 * a33; dest[12] = b30 * a00 + b31 * a10 + b32 * a20 + b33 * a30; dest[13] = b30 * a01 + b31 * a11 + b32 * a21 + b33 * a31; dest[14] = b30 * a02 + b31 * a12 + b32 * a22 + b33 * a32; dest[15] = b30 * a03 + b31 * a13 + b32 * a23 + b33 * a33; return dest; }, /** * Multiplies the two given 3x3 matrices by each other. * @method mulMat4 * @static */ mulMat3(a, b, dest) { if (!dest) { dest = new FloatArrayType(9); } const a11 = a[0]; const a12 = a[3]; const a13 = a[6]; const a21 = a[1]; const a22 = a[4]; const a23 = a[7]; const a31 = a[2]; const a32 = a[5]; const a33 = a[8]; const b11 = b[0]; const b12 = b[3]; const b13 = b[6]; const b21 = b[1]; const b22 = b[4]; const b23 = b[7]; const b31 = b[2]; const b32 = b[5]; const b33 = b[8]; dest[0] = a11 * b11 + a12 * b21 + a13 * b31; dest[3] = a11 * b12 + a12 * b22 + a13 * b32; dest[6] = a11 * b13 + a12 * b23 + a13 * b33; dest[1] = a21 * b11 + a22 * b21 + a23 * b31; dest[4] = a21 * b12 + a22 * b22 + a23 * b32; dest[7] = a21 * b13 + a22 * b23 + a23 * b33; dest[2] = a31 * b11 + a32 * b21 + a33 * b31; dest[5] = a31 * b12 + a32 * b22 + a33 * b32; dest[8] = a31 * b13 + a32 * b23 + a33 * b33; return dest; }, /** * Multiplies each element of the given 4x4 matrix by the given scalar. * @method mulMat4Scalar * @static */ mulMat4Scalar(m, s, dest) { if (!dest) { dest = m; } dest[0] = m[0] * s; dest[1] = m[1] * s; dest[2] = m[2] * s; dest[3] = m[3] * s; dest[4] = m[4] * s; dest[5] = m[5] * s; dest[6] = m[6] * s; dest[7] = m[7] * s; dest[8] = m[8] * s; dest[9] = m[9] * s; dest[10] = m[10] * s; dest[11] = m[11] * s; dest[12] = m[12] * s; dest[13] = m[13] * s; dest[14] = m[14] * s; dest[15] = m[15] * s; return dest; }, /** * Multiplies the given 4x4 matrix by the given four-element vector. * @method mulMat4v4 * @static */ mulMat4v4(m, v, dest = math.vec4()) { const v0 = v[0]; const v1 = v[1]; const v2 = v[2]; const v3 = v[3]; dest[0] = m[0] * v0 + m[4] * v1 + m[8] * v2 + m[12] * v3; dest[1] = m[1] * v0 + m[5] * v1 + m[9] * v2 + m[13] * v3; dest[2] = m[2] * v0 + m[6] * v1 + m[10] * v2 + m[14] * v3; dest[3] = m[3] * v0 + m[7] * v1 + m[11] * v2 + m[15] * v3; return dest; }, /** * Transposes the given 4x4 matrix. * @method transposeMat4 * @static */ transposeMat4(mat, dest) { // If we are transposing ourselves we can skip a few steps but have to cache some values const m4 = mat[4]; const m14 = mat[14]; const m8 = mat[8]; const m13 = mat[13]; const m12 = mat[12]; const m9 = mat[9]; if (!dest || mat === dest) { const a01 = mat[1]; const a02 = mat[2]; const a03 = mat[3]; const a12 = mat[6]; const a13 = mat[7]; const a23 = mat[11]; mat[1] = m4; mat[2] = m8; mat[3] = m12; mat[4] = a01; mat[6] = m9; mat[7] = m13; mat[8] = a02; mat[9] = a12; mat[11] = m14; mat[12] = a03; mat[13] = a13; mat[14] = a23; return mat; } dest[0] = mat[0]; dest[1] = m4; dest[2] = m8; dest[3] = m12; dest[4] = mat[1]; dest[5] = mat[5]; dest[6] = m9; dest[7] = m13; dest[8] = mat[2]; dest[9] = mat[6]; dest[10] = mat[10]; dest[11] = m14; dest[12] = mat[3]; dest[13] = mat[7]; dest[14] = mat[11]; dest[15] = mat[15]; return dest; }, /** * Transposes the given 3x3 matrix. * * @method transposeMat3 * @static */ transposeMat3(mat, dest) { if (dest === mat) { const a01 = mat[1]; const a02 = mat[2]; const a12 = mat[5]; dest[1] = mat[3]; dest[2] = mat[6]; dest[3] = a01; dest[5] = mat[7]; dest[6] = a02; dest[7] = a12; } else { dest[0] = mat[0]; dest[1] = mat[3]; dest[2] = mat[6]; dest[3] = mat[1]; dest[4] = mat[4]; dest[5] = mat[7]; dest[6] = mat[2]; dest[7] = mat[5]; dest[8] = mat[8]; } return dest; }, /** * Returns the determinant of the given 4x4 matrix. * @method determinantMat4 * @static */ determinantMat4(mat) { // Cache the matrix values (makes for huge speed increases!) const a00 = mat[0]; const a01 = mat[1]; const a02 = mat[2]; const a03 = mat[3]; const a10 = mat[4]; const a11 = mat[5]; const a12 = mat[6]; const a13 = mat[7]; const a20 = mat[8]; const a21 = mat[9]; const a22 = mat[10]; const a23 = mat[11]; const a30 = mat[12]; const a31 = mat[13]; const a32 = mat[14]; const a33 = mat[15]; return a30 * a21 * a12 * a03 - a20 * a31 * a12 * a03 - a30 * a11 * a22 * a03 + a10 * a31 * a22 * a03 + a20 * a11 * a32 * a03 - a10 * a21 * a32 * a03 - a30 * a21 * a02 * a13 + a20 * a31 * a02 * a13 + a30 * a01 * a22 * a13 - a00 * a31 * a22 * a13 - a20 * a01 * a32 * a13 + a00 * a21 * a32 * a13 + a30 * a11 * a02 * a23 - a10 * a31 * a02 * a23 - a30 * a01 * a12 * a23 + a00 * a31 * a12 * a23 + a10 * a01 * a32 * a23 - a00 * a11 * a32 * a23 - a20 * a11 * a02 * a33 + a10 * a21 * a02 * a33 + a20 * a01 * a12 * a33 - a00 * a21 * a12 * a33 - a10 * a01 * a22 * a33 + a00 * a11 * a22 * a33; }, /** * Returns the inverse of the given 4x4 matrix. * @method inverseMat4 * @static */ inverseMat4(mat, dest) { if (!dest) { dest = mat; } // Cache the matrix values (makes for huge speed increases!) const a00 = mat[0]; const a01 = mat[1]; const a02 = mat[2]; const a03 = mat[3]; const a10 = mat[4]; const a11 = mat[5]; const a12 = mat[6]; const a13 = mat[7]; const a20 = mat[8]; const a21 = mat[9]; const a22 = mat[10]; const a23 = mat[11]; const a30 = mat[12]; const a31 = mat[13]; const a32 = mat[14]; const a33 = mat[15]; const b00 = a00 * a11 - a01 * a10; const b01 = a00 * a12 - a02 * a10; const b02 = a00 * a13 - a03 * a10; const b03 = a01 * a12 - a02 * a11; const b04 = a01 * a13 - a03 * a11; const b05 = a02 * a13 - a03 * a12; const b06 = a20 * a31 - a21 * a30; const b07 = a20 * a32 - a22 * a30; const b08 = a20 * a33 - a23 * a30; const b09 = a21 * a32 - a22 * a31; const b10 = a21 * a33 - a23 * a31; const b11 = a22 * a33 - a23 * a32; // Calculate the determinant (inlined to avoid double-caching) const invDet = 1 / (b00 * b11 - b01 * b10 + b02 * b09 + b03 * b08 - b04 * b07 + b05 * b06); dest[0] = (a11 * b11 - a12 * b10 + a13 * b09) * invDet; dest[1] = (-a01 * b11 + a02 * b10 - a03 * b09) * invDet; dest[2] = (a31 * b05 - a32 * b04 + a33 * b03) * invDet; dest[3] = (-a21 * b05 + a22 * b04 - a23 * b03) * invDet; dest[4] = (-a10 * b11 + a12 * b08 - a13 * b07) * invDet; dest[5] = (a00 * b11 - a02 * b08 + a03 * b07) * invDet; dest[6] = (-a30 * b05 + a32 * b02 - a33 * b01) * invDet; dest[7] = (a20 * b05 - a22 * b02 + a23 * b01) * invDet; dest[8] = (a10 * b10 - a11 * b08 + a13 * b06) * invDet; dest[9] = (-a00 * b10 + a01 * b08 - a03 * b06) * invDet; dest[10] = (a30 * b04 - a31 * b02 + a33 * b00) * invDet; dest[11] = (-a20 * b04 + a21 * b02 - a23 * b00) * invDet; dest[12] = (-a10 * b09 + a11 * b07 - a12 * b06) * invDet; dest[13] = (a00 * b09 - a01 * b07 + a02 * b06) * invDet; dest[14] = (-a30 * b03 + a31 * b01 - a32 * b00) * invDet; dest[15] = (a20 * b03 - a21 * b01 + a22 * b00) * invDet; return dest; }, /** * Returns the trace of the given 4x4 matrix. * @method traceMat4 * @static */ traceMat4(m) { return (m[0] + m[5] + m[10] + m[15]); }, /** * Returns 4x4 translation matrix. * @method translationMat4 * @static */ translationMat4v(v, dest) { const m = dest || math.identityMat4(); m[12] = v[0]; m[13] = v[1]; m[14] = v[2]; return m; }, /** * Returns 3x3 translation matrix. * @method translationMat3 * @static */ translationMat3v(v, dest) { const m = dest || math.identityMat3(); m[6] = v[0]; m[7] = v[1]; return m; }, /** * Returns 4x4 translation matrix. * @method translationMat4c * @static */ translationMat4c: ((() => { const xyz = new FloatArrayType(3); return (x, y, z, dest) => { xyz[0] = x; xyz[1] = y; xyz[2] = z; return math.translationMat4v(xyz, dest); }; }))(), /** * Returns 4x4 translation matrix. * @method translationMat4s * @static */ translationMat4s(s, dest) { return math.translationMat4c(s, s, s, dest); }, /** * Efficiently post-concatenates a translation to the given matrix. * @param v * @param m */ translateMat4v(xyz, m) { return math.translateMat4c(xyz[0], xyz[1], xyz[2], m); }, /** * Efficiently post-concatenates a translation to the given matrix. * @param x * @param y * @param z * @param m */ translateMat4c(x, y, z, m) { const m3 = m[3]; m[0] += m3 * x; m[1] += m3 * y; m[2] += m3 * z; const m7 = m[7]; m[4] += m7 * x; m[5] += m7 * y; m[6] += m7 * z; const m11 = m[11]; m[8] += m11 * x; m[9] += m11 * y; m[10] += m11 * z; const m15 = m[15]; m[12] += m15 * x; m[13] += m15 * y; m[14] += m15 * z; return m; }, /** * Creates a new matrix that replaces the translation in the rightmost column of the given * affine matrix with the given translation. * @param m * @param translation * @param dest * @returns {*} */ setMat4Translation(m, translation, dest) { dest[0] = m[0]; dest[1] = m[1]; dest[2] = m[2]; dest[3] = m[3]; dest[4] = m[4]; dest[5] = m[5]; dest[6] = m[6]; dest[7] = m[7]; dest[8] = m[8]; dest[9] = m[9]; dest[10] = m[10]; dest[11] = m[11]; dest[12] = translation[0]; dest[13] = translation[1]; dest[14] = translation[2]; dest[15] = m[15]; return dest; }, /** * Returns 4x4 rotation matrix. * @method rotationMat4v * @static */ rotationMat4v(anglerad, axis, m) { const ax = math.normalizeVec4([axis[0], axis[1], axis[2], 0.0], []); const s = Math.sin(anglerad); const c = Math.cos(anglerad); const q = 1.0 - c; const x = ax[0]; const y = ax[1]; const z = ax[2]; let xy; let yz; let zx; let xs; let ys; let zs; //xx = x * x; used once //yy = y * y; used once //zz = z * z; used once xy = x * y; yz = y * z; zx = z * x; xs = x * s; ys = y * s; zs = z * s; m = m || math.mat4(); m[0] = (q * x * x) + c; m[1] = (q * xy) + zs; m[2] = (q * zx) - ys; m[3] = 0.0; m[4] = (q * xy) - zs; m[5] = (q * y * y) + c; m[6] = (q * yz) + xs; m[7] = 0.0; m[8] = (q * zx) + ys; m[9] = (q * yz) - xs; m[10] = (q * z * z) + c; m[11] = 0.0; m[12] = 0.0; m[13] = 0.0; m[14] = 0.0; m[15] = 1.0; return m; }, /** * Returns 4x4 rotation matrix. * @method rotationMat4c * @static */ rotationMat4c(anglerad, x, y, z, mat) { return math.rotationMat4v(anglerad, [x, y, z], mat); }, /** * Returns 4x4 scale matrix. * @method scalingMat4v * @static */ scalingMat4v(v, m = math.identityMat4()) { m[0] = v[0]; m[5] = v[1]; m[10] = v[2]; return m; }, /** * Returns 3x3 scale matrix. * @method scalingMat3v * @static */ scalingMat3v(v, m = math.identityMat3()) { m[0] = v[0]; m[4] = v[1]; return m; }, /** * Returns 4x4 scale matrix. * @method scalingMat4c * @static */ scalingMat4c: ((() => { const xyz = new FloatArrayType(3); return (x, y, z, dest) => { xyz[0] = x; xyz[1] = y; xyz[2] = z; return math.scalingMat4v(xyz, dest); }; }))(), /** * Efficiently post-concatenates a scaling to the given matrix. * @method scaleMat4c * @param x * @param y * @param z * @param m */ scaleMat4c(x, y, z, m) { m[0] *= x; m[4] *= y; m[8] *= z; m[1] *= x; m[5] *= y; m[9] *= z; m[2] *= x; m[6] *= y; m[10] *= z; m[3] *= x; m[7] *= y; m[11] *= z; return m; }, /** * Efficiently post-concatenates a scaling to the given matrix. * @method scaleMat4c * @param xyz * @param m */ scaleMat4v(xyz, m) { const x = xyz[0]; const y = xyz[1]; const z = xyz[2]; m[0] *= x; m[4] *= y; m[8] *= z; m[1] *= x; m[5] *= y; m[9] *= z; m[2] *= x; m[6] *= y; m[10] *= z; m[3] *= x; m[7] *= y; m[11] *= z; return m; }, /** * Returns 4x4 scale matrix. * @method scalingMat4s * @static */ scalingMat4s(s) { return math.scalingMat4c(s, s, s); }, /** * Creates a matrix from a quaternion rotation and vector translation * * @param {Number[]} q Rotation quaternion * @param {Number[]} v Translation vector * @param {Number[]} dest Destination matrix * @returns {Number[]} dest */ rotationTranslationMat4(q, v, dest = math.mat4()) { const x = q[0]; const y = q[1]; const z = q[2]; const w = q[3]; const x2 = x + x; const y2 = y + y; const z2 = z + z; const xx = x * x2; const xy = x * y2; const xz = x * z2; const yy = y * y2; const yz = y * z2; const zz = z * z2; const wx = w * x2; const wy = w * y2; const wz = w * z2; dest[0] = 1 - (yy + zz); dest[1] = xy + wz; dest[2] = xz - wy; dest[3] = 0; dest[4] = xy - wz; dest[5] = 1 - (xx + zz); dest[6] = yz + wx; dest[7] = 0; dest[8] = xz + wy; dest[9] = yz - wx; dest[10] = 1 - (xx + yy); dest[11] = 0; dest[12] = v[0]; dest[13] = v[1]; dest[14] = v[2]; dest[15] = 1; return dest; }, /** * Gets Euler angles from a 4x4 matrix. * * @param {Number[]} mat The 4x4 matrix. * @param {String} order Desired Euler angle order: "XYZ", "YXZ", "ZXY" etc. * @param {Number[]} [dest] Destination Euler angles, created by default. * @returns {Number[]} The Euler angles (in degrees). */ mat4ToEuler(mat, order, dest = math.vec4()) { const clamp = math.clamp; // Assumes the upper 3x3 of m is a pure rotation matrix (i.e, unscaled) const m11 = mat[0]; const m12 = mat[4]; const m13 = mat[8]; const m21 = mat[1]; const m22 = mat[5]; const m23 = mat[9]; const m31 = mat[2]; const m32 = mat[6]; const m33 = mat[10]; if (order === 'XYZ') { dest[1] = Math.asin(clamp(m13, -1, 1)); if (Math.abs(m13) < 0.99999) { dest[0] = Math.atan2(-m23, m33); dest[2] = Math.atan2(-m12, m11); } else { dest[0] = Math.atan2(m32, m22); dest[2] = 0; } } else if (order === 'YXZ') { dest[0] = Math.asin(-clamp(m23, -1, 1)); if (Math.abs(m23) < 0.99999) { dest[1] = Math.atan2(m13, m33); dest[2] = Math.atan2(m21, m22); } else { dest[1] = Math.atan2(-m31, m11); dest[2] = 0; } } else if (order === 'ZXY') { dest[0] = Math.asin(clamp(m32, -1, 1)); if (Math.abs(m32) < 0.99999) { dest[1] = Math.atan2(-m31, m33); dest[2] = Math.atan2(-m12, m22); } else { dest[1] = 0; dest[2] = Math.atan2(m21, m11); } } else if (order === 'ZYX') { dest[1] = Math.asin(-clamp(m31, -1, 1)); if (Math.abs(m31) < 0.99999) { dest[0] = Math.atan2(m32, m33); dest[2] = Math.atan2(m21, m11); } else { dest[0] = 0; dest[2] = Math.atan2(-m12, m22); } } else if (order === 'YZX') { dest[2] = Math.asin(clamp(m21, -1, 1)); if (Math.abs(m21) < 0.99999) { dest[0] = Math.atan2(-m23, m22); dest[1] = Math.atan2(-m31, m11); } else { dest[0] = 0; dest[1] = Math.atan2(m13, m33); } } else if (order === 'XZY') { dest[2] = Math.asin(-clamp(m12, -1, 1)); if (Math.abs(m12) < 0.99999) { dest[0] = Math.atan2(m32, m22); dest[1] = Math.atan2(m13, m11); } else { dest[0] = Math.atan2(-m23, m33); dest[1] = 0; } } dest[0] *= math.RADTODEG; dest[1] *= math.RADTODEG; dest[2] *= math.RADTODEG; return dest; }, composeMat4(position, quaternion, scale, mat = math.mat4()) { math.quaternionToRotationMat4(quaternion, mat); math.scaleMat4v(scale, mat); math.translateMat4v(position, mat); return mat; }, decomposeMat4: (() => { const vec = new FloatArrayType(3); const matrix = new FloatArrayType(16); return function decompose(mat, position, quaternion, scale) { vec[0] = mat[0]; vec[1] = mat[1]; vec[2] = mat[2]; let sx = math.lenVec3(vec); vec[0] = mat[4]; vec[1] = mat[5]; vec[2] = mat[6]; const sy = math.lenVec3(vec); vec[8] = mat[8]; vec[9] = mat[9]; vec[10] = mat[10]; const sz = math.lenVec3(vec); // if determine is negative, we need to invert one scale const det = math.determinantMat4(mat); if (det < 0) { sx = -sx; } position[0] = mat[12]; position[1] = mat[13]; position[2] = mat[14]; // scale the rotation part matrix.set(mat); const invSX = 1 / sx; const invSY = 1 / sy; const invSZ = 1 / sz; matrix[0] *= invSX; matrix[1] *= invSX; matrix[2] *= invSX; matrix[4] *= invSY; matrix[5] *= invSY; matrix[6] *= invSY; matrix[8] *= invSZ; matrix[9] *= invSZ; matrix[10] *= invSZ; math.mat4ToQuaternion(matrix, quaternion); scale[0] = sx; scale[1] = sy; scale[2] = sz; return this; }; })(), /** @private */ getColMat4(mat, c) { const i = c * 4; return [mat[i], mat[i + 1], mat[i + 2], mat[i + 3]]; }, /** @private */ setRowMat4(mat, r, v) { mat[r] = v[0]; mat[r + 4] = v[1]; mat[r + 8] = v[2]; mat[r + 12] = v[3]; }, /** * Returns a 4x4 'lookat' viewing transform matrix. * @method lookAtMat4v * @param pos vec3 position of the viewer * @param target vec3 point the viewer is looking at * @param up vec3 pointing "up" * @param dest mat4 Optional, mat4 matrix will be written into * * @return {mat4} dest if specified, a new mat4 otherwise */ lookAtMat4v(pos, target, up, dest) { if (!dest) { dest = math.mat4(); } const posx = pos[0]; const posy = pos[1]; const posz = pos[2]; const upx = up[0]; const upy = up[1]; const upz = up[2]; const targetx = target[0]; const targety = target[1]; const targetz = target[2]; if (posx === targetx && posy === targety && posz === targetz) { return math.identityMat4(); } let z0; let z1; let z2; let x0; let x1; let x2; let y0; let y1; let y2; let len; //vec3.direction(eye, center, z); z0 = posx - targetx; z1 = posy - targety; z2 = posz - targetz; // normalize (no check needed for 0 because of early return) len = 1 / Math.sqrt(z0 * z0 + z1 * z1 + z2 * z2); z0 *= len; z1 *= len; z2 *= len; //vec3.normalize(vec3.cross(up, z, x)); x0 = upy * z2 - upz * z1; x1 = upz * z0 - upx * z2; x2 = upx * z1 - upy * z0; len = Math.sqrt(x0 * x0 + x1 * x1 + x2 * x2); if (!len) { x0 = 0; x1 = 0; x2 = 0; } else { len = 1 / len; x0 *= len; x1 *= len; x2 *= len; } //vec3.normalize(vec3.cross(z, x, y)); y0 = z1 * x2 - z2 * x1; y1 = z2 * x0 - z0 * x2; y2 = z0 * x1 - z1 * x0; len = Math.sqrt(y0 * y0 + y1 * y1 + y2 * y2); if (!len) { y0 = 0; y1 = 0; y2 = 0; } else { len = 1 / len; y0 *= len; y1 *= len; y2 *= len; } dest[0] = x0; dest[1] = y0; dest[2] = z0; dest[3] = 0; dest[4] = x1; dest[5] = y1; dest[6] = z1; dest[7] = 0; dest[8] = x2; dest[9] = y2; dest[10] = z2; dest[11] = 0; dest[12] = -(x0 * posx + x1 * posy + x2 * posz); dest[13] = -(y0 * posx + y1 * posy + y2 * posz); dest[14] = -(z0 * posx + z1 * posy + z2 * posz); dest[15] = 1; return dest; }, /** * Returns a 4x4 'lookat' viewing transform matrix. * @method lookAtMat4c * @static */ lookAtMat4c(posx, posy, posz, targetx, targety, targetz, upx, upy, upz) { return math.lookAtMat4v([posx, posy, posz], [targetx, targety, targetz], [upx, upy, upz], []); }, /** * Returns a 4x4 orthographic projection matrix. * @method orthoMat4c * @static */ orthoMat4c(left, right, bottom, top, near, far, dest) { if (!dest) { dest = math.mat4(); } const rl = (right - left); const tb = (top - bottom); const fn = (far - near); dest[0] = 2.0 / rl; dest[1] = 0.0; dest[2] = 0.0; dest[3] = 0.0; dest[4] = 0.0; dest[5] = 2.0 / tb; dest[6] = 0.0; dest[7] = 0.0; dest[8] = 0.0; dest[9] = 0.0; dest[10] = -2.0 / fn; dest[11] = 0.0; dest[12] = -(left + right) / rl; dest[13] = -(top + bottom) / tb; dest[14] = -(far + near) / fn; dest[15] = 1.0; return dest; }, /** * Returns a 4x4 perspective projection matrix. * @method frustumMat4v * @static */ frustumMat4v(fmin, fmax, m) { if (!m) { m = math.mat4(); } const fmin4 = [fmin[0], fmin[1], fmin[2], 0.0]; const fmax4 = [fmax[0], fmax[1], fmax[2], 0.0]; math.addVec4(fmax4, fmin4, tempMat1); math.subVec4(fmax4, fmin4, tempMat2); const t = 2.0 * fmin4[2]; const tempMat20 = tempMat2[0]; const tempMat21 = tempMat2[1]; const tempMat22 = tempMat2[2]; m[0] = t / tempMat20; m[1] = 0.0; m[2] = 0.0; m[3] = 0.0; m[4] = 0.0; m[5] = t / tempMat21; m[6] = 0.0; m[7] = 0.0; m[8] = tempMat1[0] / tempMat20; m[9] = tempMat1[1] / tempMat21; m[10] = -tempMat1[2] / tempMat22; m[11] = -1.0; m[12] = 0.0; m[13] = 0.0; m[14] = -t * fmax4[2] / tempMat22; m[15] = 0.0; return m; }, /** * Returns a 4x4 perspective projection matrix. * @method frustumMat4v * @static */ frustumMat4(left, right, bottom, top, near, far, dest) { if (!dest) { dest = math.mat4(); } const rl = (right - left); const tb = (top - bottom); const fn = (far - near); dest[0] = (near * 2) / rl; dest[1] = 0; dest[2] = 0; dest[3] = 0; dest[4] = 0; dest[5] = (near * 2) / tb; dest[6] = 0; dest[7] = 0; dest[8] = (right + left) / rl; dest[9] = (top + bottom) / tb; dest[10] = -(far + near) / fn; dest[11] = -1; dest[12] = 0; dest[13] = 0; dest[14] = -(far * near * 2) / fn; dest[15] = 0; return dest; }, /** * Returns a 4x4 perspective projection matrix. * @method perspectiveMat4v * @static */ perspectiveMat4(fovyrad, aspectratio, znear, zfar, m) { const pmin = []; const pmax = []; pmin[2] = znear; pmax[2] = zfar; pmax[1] = pmin[2] * Math.tan(fovyrad / 2.0); pmin[1] = -pmax[1]; pmax[0] = pmax[1] * aspectratio; pmin[0] = -pmax[0]; return math.frustumMat4v(pmin, pmax, m); }, /** * Returns true if the two 4x4 matrices are the same. * @param m1 * @param m2 * @returns {Boolean} */ compareMat4(m1, m2) { return m1[0] === m2[0] && m1[1] === m2[1] && m1[2] === m2[2] && m1[3] === m2[3] && m1[4] === m2[4] && m1[5] === m2[5] && m1[6] === m2[6] && m1[7] === m2[7] && m1[8] === m2[8] && m1[9] === m2[9] && m1[10] === m2[10] && m1[11] === m2[11] && m1[12] === m2[12] && m1[13] === m2[13] && m1[14] === m2[14] && m1[15] === m2[15]; }, /** * Transforms a three-element position by a 4x4 matrix. * @method transformPoint3 * @static */ transformPoint3(m, p, dest = math.vec3()) { const x = p[0]; const y = p[1]; const z = p[2]; dest[0] = (m[0] * x) + (m[4] * y) + (m[8] * z) + m[12]; dest[1] = (m[1] * x) + (m[5] * y) + (m[9] * z) + m[13]; dest[2] = (m[2] * x) + (m[6] * y) + (m[10] * z) + m[14]; return dest; }, /** * Transforms a homogeneous coordinate by a 4x4 matrix. * @method transformPoint3 * @static */ transformPoint4(m, v, dest = math.vec4()) { dest[0] = m[0] * v[0] + m[4] * v[1] + m[8] * v[2] + m[12] * v[3]; dest[1] = m[1] * v[0] + m[5] * v[1] + m[9] * v[2] + m[13] * v[3]; dest[2] = m[2] * v[0] + m[6] * v[1] + m[10] * v[2] + m[14] * v[3]; dest[3] = m[3] * v[0] + m[7] * v[1] + m[11] * v[2] + m[15] * v[3]; return dest; }, /** * Transforms an array of three-element positions by a 4x4 matrix. * @method transformPoints3 * @static */ transformPoints3(m, points, points2) { const result = points2 || []; const len = points.length; let p0; let p1; let p2; let pi; // cache values const m0 = m[0]; const m1 = m[1]; const m2 = m[2]; const m3 = m[3]; const m4 = m[4]; const m5 = m[5]; const m6 = m[6]; const m7 = m[7]; const m8 = m[8]; const m9 = m[9]; const m10 = m[10]; const m11 = m[11]; const m12 = m[12]; const m13 = m[13]; const m14 = m[14]; const m15 = m[15]; let r; for (let i = 0; i < len; ++i) { // cache values pi = points[i]; p0 = pi[0]; p1 = pi[1]; p2 = pi[2]; r = result[i] || (result[i] = [0, 0, 0]); r[0] = (m0 * p0) + (m4 * p1) + (m8 * p2) + m12; r[1] = (m1 * p0) + (m5 * p1) + (m9 * p2) + m13; r[2] = (m2 * p0) + (m6 * p1) + (m10 * p2) + m14; r[3] = (m3 * p0) + (m7 * p1) + (m11 * p2) + m15; } result.length = len; return result; }, /** * Transforms an array of positions by a 4x4 matrix. * @method transformPositions3 * @static */ transformPositions3(m, p, p2 = p) { let i; const len = p.length; let x; let y; let z; const m0 = m[0]; const m1 = m[1]; const m2 = m[2]; m[3]; const m4 = m[4]; const m5 = m[5]; const m6 = m[6]; m[7]; const m8 = m[8]; const m9 = m[9]; const m10 = m[10]; m[11]; const m12 = m[12]; const m13 = m[13]; const m14 = m[14]; m[15]; for (i = 0; i < len; i += 3) { x = p[i + 0]; y = p[i + 1]; z = p[i + 2]; p2[i + 0] = (m0 * x) + (m4 * y) + (m8 * z) + m12; p2[i + 1] = (m1 * x) + (m5 * y) + (m9 * z) + m13; p2[i + 2] = (m2 * x) + (m6 * y) + (m10 * z) + m14; } return p2; }, /** * Transforms an array of positions by a 4x4 matrix. * @method transformPositions4 * @static */ transformPositions4(m, p, p2 = p) { let i; const len = p.length; let x; let y; let z; const m0 = m[0]; const m1 = m[1]; const m2 = m[2]; const m3 = m[3]; const m4 = m[4]; const m5 = m[5]; const m6 = m[6]; const m7 = m[7]; const m8 = m[8]; const m9 = m[9]; const m10 = m[10]; const m11 = m[11]; const m12 = m[12]; const m13 = m[13]; const m14 = m[14]; const m15 = m[15]; for (i = 0; i < len; i += 4) { x = p[i + 0]; y = p[i + 1]; z = p[i + 2]; p2[i + 0] = (m0 * x) + (m4 * y) + (m8 * z) + m12; p2[i + 1] = (m1 * x) + (m5 * y) + (m9 * z) + m13; p2[i + 2] = (m2 * x) + (m6 * y) + (m10 * z) + m14; p2[i + 3] = (m3 * x) + (m7 * y) + (m11 * z) + m15; } return p2; }, /** * Transforms a three-element vector by a 4x4 matrix. * @method transformVec3 * @static */ transformVec3(m, v, dest) { const v0 = v[0]; const v1 = v[1]; const v2 = v[2]; dest = dest || this.vec3(); dest[0] = (m[0] * v0) + (m[4] * v1) + (m[8] * v2); dest[1] = (m[1] * v0) + (m[5] * v1) + (m[9] * v2); dest[2] = (m[2] * v0) + (m[6] * v1) + (m[10] * v2); return dest; }, /** * Transforms a four-element vector by a 4x4 matrix. * @method transformVec4 * @static */ transformVec4(m, v, dest) { const v0 = v[0]; const v1 = v[1]; const v2 = v[2]; const v3 = v[3]; dest = dest || math.vec4(); dest[0] = m[0] * v0 + m[4] * v1 + m[8] * v2 + m[12] * v3; dest[1] = m[1] * v0 + m[5] * v1 + m[9] * v2 + m[13] * v3; dest[2] = m[2] * v0 + m[6] * v1 + m[10] * v2 + m[14] * v3; dest[3] = m[3] * v0 + m[7] * v1 + m[11] * v2 + m[15] * v3; return dest; }, /** * Rotate a 2D vector around a center point. * * @param a * @param center * @param angle * @returns {math} */ rotateVec2(a, center, angle, dest = a) { const c = Math.cos(angle); const s = Math.sin(angle); const x = a[0] - center[0]; const y = a[1] - center[1]; dest[0] = x * c - y * s + center[0]; dest[1] = x * s + y * c + center[1]; return a; }, /** * Rotate a 3D vector around the x-axis * * @method rotateVec3X * @param {Number[]} a The vec3 point to rotate * @param {Number[]} b The origin of the rotation * @param {Number} c The angle of rotation * @param {Number[]} dest The receiving vec3 * @returns {Number[]} dest * @static */ rotateVec3X(a, b, c, dest) { const p = []; const r = []; //Translate point to the origin p[0] = a[0] - b[0]; p[1] = a[1] - b[1]; p[2] = a[2] - b[2]; //perform rotation r[0] = p[0]; r[1] = p[1] * Math.cos(c) - p[2] * Math.sin(c); r[2] = p[1] * Math.sin(c) + p[2] * Math.cos(c); //translate to correct position dest[0] = r[0] + b[0]; dest[1] = r[1] + b[1]; dest[2] = r[2] + b[2]; return dest; }, /** * Rotate a 3D vector around the y-axis * * @method rotateVec3Y * @param {Number[]} a The vec3 point to rotate * @param {Number[]} b The origin of the rotation * @param {Number} c The angle of rotation * @param {Number[]} dest The receiving vec3 * @returns {Number[]} dest * @static */ rotateVec3Y(a, b, c, dest) { const p = []; const r = []; //Translate point to the origin p[0] = a[0] - b[0]; p[1] = a[1] - b[1]; p[2] = a[2] - b[2]; //perform rotation r[0] = p[2] * Math.sin(c) + p[0] * Math.cos(c); r[1] = p[1]; r[2] = p[2] * Math.cos(c) - p[0] * Math.sin(c); //translate to correct position dest[0] = r[0] + b[0]; dest[1] = r[1] + b[1]; dest[2] = r[2] + b[2]; return dest; }, /** * Rotate a 3D vector around the z-axis * * @method rotateVec3Z * @param {Number[]} a The vec3 point to rotate * @param {Number[]} b The origin of the rotation * @param {Number} c The angle of rotation * @param {Number[]} dest The receiving vec3 * @returns {Number[]} dest * @static */ rotateVec3Z(a, b, c, dest) { const p = []; const r = []; //Translate point to the origin p[0] = a[0] - b[0]; p[1] = a[1] - b[1]; p[2] = a[2] - b[2]; //perform rotation r[0] = p[0] * Math.cos(c) - p[1] * Math.sin(c); r[1] = p[0] * Math.sin(c) + p[1] * Math.cos(c); r[2] = p[2]; //translate to correct position dest[0] = r[0] + b[0]; dest[1] = r[1] + b[1]; dest[2] = r[2] + b[2]; return dest; }, /** * Transforms a four-element vector by a 4x4 projection matrix. * * @method projectVec4 * @param {Number[]} p 3D View-space coordinate * @param {Number[]} q 2D Projected coordinate * @returns {Number[]} 2D Projected coordinate * @static */ projectVec4(p, q) { const f = 1.0 / p[3]; q = q || math.vec2(); q[0] = p[0] * f; q[1] = p[1] * f; return q; }, /** * Unprojects a three-element vector. * * @method unprojectVec3 * @param {Number[]} p 3D Projected coordinate * @param {Number[]} viewMat View matrix * @returns {Number[]} projMat Projection matrix * @static */ unprojectVec3: ((() => { const mat = new FloatArrayType(16); const mat2 = new FloatArrayType(16); const mat3 = new FloatArrayType(16); return function (p, viewMat, projMat, q) { return this.transformVec3(this.mulMat4(this.inverseMat4(viewMat, mat), this.inverseMat4(projMat, mat2), mat3), p, q) }; }))(), /** * Linearly interpolates between two 3D vectors. * @method lerpVec3 * @static */ lerpVec3(t, t1, t2, p1, p2, dest) { const result = dest || math.vec3(); const f = (t - t1) / (t2 - t1); result[0] = p1[0] + (f * (p2[0] - p1[0])); result[1] = p1[1] + (f * (p2[1] - p1[1])); result[2] = p1[2] + (f * (p2[2] - p1[2])); return result; }, /** * Linearly interpolates between two 4x4 matrices. * @method lerpMat4 * @static */ lerpMat4(t, t1, t2, m1, m2, dest) { const result = dest || math.mat4(); const f = (t - t1) / (t2 - t1); result[0] = m1[0] + (f * (m2[0] - m1[0])); result[1] = m1[1] + (f * (m2[1] - m1[1])); result[2] = m1[2] + (f * (m2[2] - m1[2])); result[3] = m1[3] + (f * (m2[3] - m1[3])); result[4] = m1[4] + (f * (m2[4] - m1[4])); result[5] = m1[5] + (f * (m2[5] - m1[5])); result[6] = m1[6] + (f * (m2[6] - m1[6])); result[7] = m1[7] + (f * (m2[7] - m1[7])); result[8] = m1[8] + (f * (m2[8] - m1[8])); result[9] = m1[9] + (f * (m2[9] - m1[9])); result[10] = m1[10] + (f * (m2[10] - m1[10])); result[11] = m1[11] + (f * (m2[11] - m1[11])); result[12] = m1[12] + (f * (m2[12] - m1[12])); result[13] = m1[13] + (f * (m2[13] - m1[13])); result[14] = m1[14] + (f * (m2[14] - m1[14])); result[15] = m1[15] + (f * (m2[15] - m1[15])); return result; }, /** * Flattens a two-dimensional array into a one-dimensional array. * * @method flatten * @static * @param {Array of Arrays} a A 2D array * @returns Flattened 1D array */ flatten(a) { const result = []; let i; let leni; let j; let lenj; let item; for (i = 0, leni = a.length; i < leni; i++) { item = a[i]; for (j = 0, lenj = item.length; j < lenj; j++) { result.push(item[j]); } } return result; }, identityQuaternion(dest = math.vec4()) { dest[0] = 0.0; dest[1] = 0.0; dest[2] = 0.0; dest[3] = 1.0; return dest; }, /** * Initializes a quaternion from Euler angles. * * @param {Number[]} euler The Euler angles (in degrees). * @param {String} order Euler angle order: "XYZ", "YXZ", "ZXY" etc. * @param {Number[]} [dest] Destination quaternion, created by default. * @returns {Number[]} The quaternion. */ eulerToQuaternion(euler, order, dest = math.vec4()) { // http://www.mathworks.com/matlabcentral/fileexchange/ // 20696-function-to-convert-between-dcm-euler-angles-quaternions-and-euler-vectors/ // content/SpinCalc.m const a = (euler[0] * math.DEGTORAD) / 2; const b = (euler[1] * math.DEGTORAD) / 2; const c = (euler[2] * math.DEGTORAD) / 2; const c1 = Math.cos(a); const c2 = Math.cos(b); const c3 = Math.cos(c); const s1 = Math.sin(a); const s2 = Math.sin(b); const s3 = Math.sin(c); if (order === 'XYZ') { dest[0] = s1 * c2 * c3 + c1 * s2 * s3; dest[1] = c1 * s2 * c3 - s1 * c2 * s3; dest[2] = c1 * c2 * s3 + s1 * s2 * c3; dest[3] = c1 * c2 * c3 - s1 * s2 * s3; } else if (order === 'YXZ') { dest[0] = s1 * c2 * c3 + c1 * s2 * s3; dest[1] = c1 * s2 * c3 - s1 * c2 * s3; dest[2] = c1 * c2 * s3 - s1 * s2 * c3; dest[3] = c1 * c2 * c3 + s1 * s2 * s3; } else if (order === 'ZXY') { dest[0] = s1 * c2 * c3 - c1 * s2 * s3; dest[1] = c1 * s2 * c3 + s1 * c2 * s3; dest[2] = c1 * c2 * s3 + s1 * s2 * c3; dest[3] = c1 * c2 * c3 - s1 * s2 * s3; } else if (order === 'ZYX') { dest[0] = s1 * c2 * c3 - c1 * s2 * s3; dest[1] = c1 * s2 * c3 + s1 * c2 * s3; dest[2] = c1 * c2 * s3 - s1 * s2 * c3; dest[3] = c1 * c2 * c3 + s1 * s2 * s3; } else if (order === 'YZX') { dest[0] = s1 * c2 * c3 + c1 * s2 * s3; dest[1] = c1 * s2 * c3 + s1 * c2 * s3; dest[2] = c1 * c2 * s3 - s1 * s2 * c3; dest[3] = c1 * c2 * c3 - s1 * s2 * s3; } else if (order === 'XZY') { dest[0] = s1 * c2 * c3 - c1 * s2 * s3; dest[1] = c1 * s2 * c3 - s1 * c2 * s3; dest[2] = c1 * c2 * s3 + s1 * s2 * c3; dest[3] = c1 * c2 * c3 + s1 * s2 * s3; } return dest; }, mat4ToQuaternion(m, dest = math.vec4()) { // http://www.euclideanspace.com/maths/geometry/rotations/conversions/matrixToQuaternion/index.htm // Assumes the upper 3x3 of m is a pure rotation matrix (i.e, unscaled) const m11 = m[0]; const m12 = m[4]; const m13 = m[8]; const m21 = m[1]; const m22 = m[5]; const m23 = m[9]; const m31 = m[2]; const m32 = m[6]; const m33 = m[10]; let s; const trace = m11 + m22 + m33; if (trace > 0) { s = 0.5 / Math.sqrt(trace + 1.0); dest[3] = 0.25 / s; dest[0] = (m32 - m23) * s; dest[1] = (m13 - m31) * s; dest[2] = (m21 - m12) * s; } else if (m11 > m22 && m11 > m33) { s = 2.0 * Math.sqrt(1.0 + m11 - m22 - m33); dest[3] = (m32 - m23) / s; dest[0] = 0.25 * s; dest[1] = (m12 + m21) / s; dest[2] = (m13 + m31) / s; } else if (m22 > m33) { s = 2.0 * Math.sqrt(1.0 + m22 - m11 - m33); dest[3] = (m13 - m31) / s; dest[0] = (m12 + m21) / s; dest[1] = 0.25 * s; dest[2] = (m23 + m32) / s; } else { s = 2.0 * Math.sqrt(1.0 + m33 - m11 - m22); dest[3] = (m21 - m12) / s; dest[0] = (m13 + m31) / s; dest[1] = (m23 + m32) / s; dest[2] = 0.25 * s; } return dest; }, vec3PairToQuaternion(u, v, dest = math.vec4()) { const norm_u_norm_v = Math.sqrt(math.dotVec3(u, u) * math.dotVec3(v, v)); let real_part = norm_u_norm_v + math.dotVec3(u, v); if (real_part < 0.00000001 * norm_u_norm_v) { // If u and v are exactly opposite, rotate 180 degrees // around an arbitrary orthogonal axis. Axis normalisation // can happen later, when we normalise the quaternion. real_part = 0.0; if (Math.abs(u[0]) > Math.abs(u[2])) { dest[0] = -u[1]; dest[1] = u[0]; dest[2] = 0; } else { dest[0] = 0; dest[1] = -u[2]; dest[2] = u[1]; } } else { // Otherwise, build quaternion the standard way. math.cross3Vec3(u, v, dest); } dest[3] = real_part; return math.normalizeQuaternion(dest); }, angleAxisToQuaternion(angleAxis, dest = math.vec4()) { const halfAngle = angleAxis[3] / 2.0; const fsin = Math.sin(halfAngle); dest[0] = fsin * angleAxis[0]; dest[1] = fsin * angleAxis[1]; dest[2] = fsin * angleAxis[2]; dest[3] = Math.cos(halfAngle); return dest; }, quaternionToEuler: ((() => { const mat = new FloatArrayType(16); return (q, order, dest) => { dest = dest || math.vec3(); math.quaternionToRotationMat4(q, mat); math.mat4ToEuler(mat, order, dest); return dest; }; }))(), mulQuaternions(p, q, dest = math.vec4()) { const p0 = p[0]; const p1 = p[1]; const p2 = p[2]; const p3 = p[3]; const q0 = q[0]; const q1 = q[1]; const q2 = q[2]; const q3 = q[3]; dest[0] = p3 * q0 + p0 * q3 + p1 * q2 - p2 * q1; dest[1] = p3 * q1 + p1 * q3 + p2 * q0 - p0 * q2; dest[2] = p3 * q2 + p2 * q3 + p0 * q1 - p1 * q0; dest[3] = p3 * q3 - p0 * q0 - p1 * q1 - p2 * q2; return dest; }, vec3ApplyQuaternion(q, vec, dest = math.vec3()) { const x = vec[0]; const y = vec[1]; const z = vec[2]; const qx = q[0]; const qy = q[1]; const qz = q[2]; const qw = q[3]; // calculate quat * vector const ix = qw * x + qy * z - qz * y; const iy = qw * y + qz * x - qx * z; const iz = qw * z + qx * y - qy * x; const iw = -qx * x - qy * y - qz * z; // calculate result * inverse quat dest[0] = ix * qw + iw * -qx + iy * -qz - iz * -qy; dest[1] = iy * qw + iw * -qy + iz * -qx - ix * -qz; dest[2] = iz * qw + iw * -qz + ix * -qy - iy * -qx; return dest; }, quaternionToMat4(q, dest) { dest = math.identityMat4(dest); const q0 = q[0]; //x const q1 = q[1]; //y const q2 = q[2]; //z const q3 = q[3]; //w const tx = 2.0 * q0; const ty = 2.0 * q1; const tz = 2.0 * q2; const twx = tx * q3; const twy = ty * q3; const twz = tz * q3; const txx = tx * q0; const txy = ty * q0; const txz = tz * q0; const tyy = ty * q1; const tyz = tz * q1; const tzz = tz * q2; dest[0] = 1.0 - (tyy + tzz); dest[1] = txy + twz; dest[2] = txz - twy; dest[4] = txy - twz; dest[5] = 1.0 - (txx + tzz); dest[6] = tyz + twx; dest[8] = txz + twy; dest[9] = tyz - twx; dest[10] = 1.0 - (txx + tyy); return dest; }, quaternionToRotationMat4(q, m) { const x = q[0]; const y = q[1]; const z = q[2]; const w = q[3]; const x2 = x + x; const y2 = y + y; const z2 = z + z; const xx = x * x2; const xy = x * y2; const xz = x * z2; const yy = y * y2; const yz = y * z2; const zz = z * z2; const wx = w * x2; const wy = w * y2; const wz = w * z2; m[0] = 1 - (yy + zz); m[4] = xy - wz; m[8] = xz + wy; m[1] = xy + wz; m[5] = 1 - (xx + zz); m[9] = yz - wx; m[2] = xz - wy; m[6] = yz + wx; m[10] = 1 - (xx + yy); // last column m[3] = 0; m[7] = 0; m[11] = 0; // bottom row m[12] = 0; m[13] = 0; m[14] = 0; m[15] = 1; return m; }, normalizeQuaternion(q, dest = q) { const len = math.lenVec4([q[0], q[1], q[2], q[3]]); dest[0] = q[0] / len; dest[1] = q[1] / len; dest[2] = q[2] / len; dest[3] = q[3] / len; return dest; }, conjugateQuaternion(q, dest = q) { dest[0] = -q[0]; dest[1] = -q[1]; dest[2] = -q[2]; dest[3] = q[3]; return dest; }, inverseQuaternion(q, dest) { return math.normalizeQuaternion(math.conjugateQuaternion(q, dest)); }, quaternionToAngleAxis(q, angleAxis = math.vec4()) { q = math.normalizeQuaternion(q, tempVec4$2); const q3 = q[3]; const angle = 2 * Math.acos(q3); const s = Math.sqrt(1 - q3 * q3); if (s < 0.001) { // test to avoid divide by zero, s is always positive due to sqrt angleAxis[0] = q[0]; angleAxis[1] = q[1]; angleAxis[2] = q[2]; } else { angleAxis[0] = q[0] / s; angleAxis[1] = q[1] / s; angleAxis[2] = q[2] / s; } angleAxis[3] = angle; // * 57.295779579; return angleAxis; }, //------------------------------------------------------------------------------------------------------------------ // Boundaries //------------------------------------------------------------------------------------------------------------------ /** * Returns a new, uninitialized 3D axis-aligned bounding box. * * @private */ AABB3(values) { return new FloatArrayType(values || 6); }, /** * Returns a new, uninitialized 2D axis-aligned bounding box. * * @private */ AABB2(values) { return new FloatArrayType(values || 4); }, /** * Returns a new, uninitialized 3D oriented bounding box (OBB). * * @private */ OBB3(values) { return new FloatArrayType(values || 32); }, /** * Returns a new, uninitialized 2D oriented bounding box (OBB). * * @private */ OBB2(values) { return new FloatArrayType(values || 16); }, /** Returns a new 3D bounding sphere */ Sphere3(x, y, z, r) { return new FloatArrayType([x, y, z, r]); }, /** * Transforms an OBB3 by a 4x4 matrix. * * @private */ transformOBB3(m, p, p2 = p) { let i; const len = p.length; let x; let y; let z; const m0 = m[0]; const m1 = m[1]; const m2 = m[2]; const m3 = m[3]; const m4 = m[4]; const m5 = m[5]; const m6 = m[6]; const m7 = m[7]; const m8 = m[8]; const m9 = m[9]; const m10 = m[10]; const m11 = m[11]; const m12 = m[12]; const m13 = m[13]; const m14 = m[14]; const m15 = m[15]; for (i = 0; i < len; i += 4) { x = p[i + 0]; y = p[i + 1]; z = p[i + 2]; p2[i + 0] = (m0 * x) + (m4 * y) + (m8 * z) + m12; p2[i + 1] = (m1 * x) + (m5 * y) + (m9 * z) + m13; p2[i + 2] = (m2 * x) + (m6 * y) + (m10 * z) + m14; p2[i + 3] = (m3 * x) + (m7 * y) + (m11 * z) + m15; } return p2; }, /** Returns true if the first AABB contains the second AABB. * @param aabb1 * @param aabb2 * @returns {Boolean} */ containsAABB3: function (aabb1, aabb2) { const result = ( aabb1[0] <= aabb2[0] && aabb2[3] <= aabb1[3] && aabb1[1] <= aabb2[1] && aabb2[4] <= aabb1[4] && aabb1[2] <= aabb2[2] && aabb2[5] <= aabb1[5]); return result; }, /** * Gets the diagonal size of an AABB3 given as minima and maxima. * * @private */ getAABB3Diag: ((() => { const min = new FloatArrayType(3); const max = new FloatArrayType(3); const tempVec3 = new FloatArrayType(3); return aabb => { min[0] = aabb[0]; min[1] = aabb[1]; min[2] = aabb[2]; max[0] = aabb[3]; max[1] = aabb[4]; max[2] = aabb[5]; math.subVec3(max, min, tempVec3); return Math.abs(math.lenVec3(tempVec3)); }; }))(), /** * Get a diagonal boundary size that is symmetrical about the given point. * * @private */ getAABB3DiagPoint: ((() => { const min = new FloatArrayType(3); const max = new FloatArrayType(3); const tempVec3 = new FloatArrayType(3); return (aabb, p) => { min[0] = aabb[0]; min[1] = aabb[1]; min[2] = aabb[2]; max[0] = aabb[3]; max[1] = aabb[4]; max[2] = aabb[5]; const diagVec = math.subVec3(max, min, tempVec3); const xneg = p[0] - aabb[0]; const xpos = aabb[3] - p[0]; const yneg = p[1] - aabb[1]; const ypos = aabb[4] - p[1]; const zneg = p[2] - aabb[2]; const zpos = aabb[5] - p[2]; diagVec[0] += (xneg > xpos) ? xneg : xpos; diagVec[1] += (yneg > ypos) ? yneg : ypos; diagVec[2] += (zneg > zpos) ? zneg : zpos; return Math.abs(math.lenVec3(diagVec)); }; }))(), /** * Gets the area of an AABB. * * @private */ getAABB3Area(aabb) { const width = (aabb[3] - aabb[0]); const height = (aabb[4] - aabb[1]); const depth = (aabb[5] - aabb[2]); return (width * height * depth); }, /** * Gets the center of an AABB. * * @private */ getAABB3Center(aabb, dest) { const r = dest || math.vec3(); r[0] = (aabb[0] + aabb[3]) / 2; r[1] = (aabb[1] + aabb[4]) / 2; r[2] = (aabb[2] + aabb[5]) / 2; return r; }, /** * Gets the center of a 2D AABB. * * @private */ getAABB2Center(aabb, dest) { const r = dest || math.vec2(); r[0] = (aabb[2] + aabb[0]) / 2; r[1] = (aabb[3] + aabb[1]) / 2; return r; }, /** * Collapses a 3D axis-aligned boundary, ready to expand to fit 3D points. * Creates new AABB if none supplied. * * @private */ collapseAABB3(aabb = math.AABB3()) { aabb[0] = math.MAX_DOUBLE; aabb[1] = math.MAX_DOUBLE; aabb[2] = math.MAX_DOUBLE; aabb[3] = math.MIN_DOUBLE; aabb[4] = math.MIN_DOUBLE; aabb[5] = math.MIN_DOUBLE; return aabb; }, /** * Converts an axis-aligned 3D boundary into an oriented boundary consisting of * an array of eight 3D positions, one for each corner of the boundary. * * @private */ AABB3ToOBB3(aabb, obb = math.OBB3()) { obb[0] = aabb[0]; obb[1] = aabb[1]; obb[2] = aabb[2]; obb[3] = 1; obb[4] = aabb[3]; obb[5] = aabb[1]; obb[6] = aabb[2]; obb[7] = 1; obb[8] = aabb[3]; obb[9] = aabb[4]; obb[10] = aabb[2]; obb[11] = 1; obb[12] = aabb[0]; obb[13] = aabb[4]; obb[14] = aabb[2]; obb[15] = 1; obb[16] = aabb[0]; obb[17] = aabb[1]; obb[18] = aabb[5]; obb[19] = 1; obb[20] = aabb[3]; obb[21] = aabb[1]; obb[22] = aabb[5]; obb[23] = 1; obb[24] = aabb[3]; obb[25] = aabb[4]; obb[26] = aabb[5]; obb[27] = 1; obb[28] = aabb[0]; obb[29] = aabb[4]; obb[30] = aabb[5]; obb[31] = 1; return obb; }, /** * Finds the minimum axis-aligned 3D boundary enclosing the homogeneous 3D points (x,y,z,w) given in a flattened array. * * @private */ positions3ToAABB3: ((() => { const p = new FloatArrayType(3); return (positions, aabb, positionsDecodeMatrix) => { aabb = aabb || math.AABB3(); let xmin = math.MAX_DOUBLE; let ymin = math.MAX_DOUBLE; let zmin = math.MAX_DOUBLE; let xmax = math.MIN_DOUBLE; let ymax = math.MIN_DOUBLE; let zmax = math.MIN_DOUBLE; let x; let y; let z; for (let i = 0, len = positions.length; i < len; i += 3) { if (positionsDecodeMatrix) { p[0] = positions[i + 0]; p[1] = positions[i + 1]; p[2] = positions[i + 2]; math.decompressPosition(p, positionsDecodeMatrix, p); x = p[0]; y = p[1]; z = p[2]; } else { x = positions[i + 0]; y = positions[i + 1]; z = positions[i + 2]; } if (x < xmin) { xmin = x; } if (y < ymin) { ymin = y; } if (z < zmin) { zmin = z; } if (x > xmax) { xmax = x; } if (y > ymax) { ymax = y; } if (z > zmax) { zmax = z; } } aabb[0] = xmin; aabb[1] = ymin; aabb[2] = zmin; aabb[3] = xmax; aabb[4] = ymax; aabb[5] = zmax; return aabb; }; }))(), /** * Finds the minimum axis-aligned 3D boundary enclosing the homogeneous 3D points (x,y,z,w) given in a flattened array. * * @private */ OBB3ToAABB3(obb, aabb = math.AABB3()) { let xmin = math.MAX_DOUBLE; let ymin = math.MAX_DOUBLE; let zmin = math.MAX_DOUBLE; let xmax = math.MIN_DOUBLE; let ymax = math.MIN_DOUBLE; let zmax = math.MIN_DOUBLE; let x; let y; let z; for (let i = 0, len = obb.length; i < len; i += 4) { x = obb[i + 0]; y = obb[i + 1]; z = obb[i + 2]; if (x < xmin) { xmin = x; } if (y < ymin) { ymin = y; } if (z < zmin) { zmin = z; } if (x > xmax) { xmax = x; } if (y > ymax) { ymax = y; } if (z > zmax) { zmax = z; } } aabb[0] = xmin; aabb[1] = ymin; aabb[2] = zmin; aabb[3] = xmax; aabb[4] = ymax; aabb[5] = zmax; return aabb; }, /** * Finds the minimum axis-aligned 3D boundary enclosing the given 3D points. * * @private */ points3ToAABB3(points, aabb = math.AABB3()) { let xmin = math.MAX_DOUBLE; let ymin = math.MAX_DOUBLE; let zmin = math.MAX_DOUBLE; let xmax = math.MIN_DOUBLE; let ymax = math.MIN_DOUBLE; let zmax = math.MIN_DOUBLE; let x; let y; let z; for (let i = 0, len = points.length; i < len; i++) { x = points[i][0]; y = points[i][1]; z = points[i][2]; if (x < xmin) { xmin = x; } if (y < ymin) { ymin = y; } if (z < zmin) { zmin = z; } if (x > xmax) { xmax = x; } if (y > ymax) { ymax = y; } if (z > zmax) { zmax = z; } } aabb[0] = xmin; aabb[1] = ymin; aabb[2] = zmin; aabb[3] = xmax; aabb[4] = ymax; aabb[5] = zmax; return aabb; }, /** * Finds the minimum boundary sphere enclosing the given 3D points. * * @private */ points3ToSphere3: ((() => { const tempVec3 = new FloatArrayType(3); return (points, sphere) => { sphere = sphere || math.vec4(); let x = 0; let y = 0; let z = 0; let i; const numPoints = points.length; for (i = 0; i < numPoints; i++) { x += points[i][0]; y += points[i][1]; z += points[i][2]; } sphere[0] = x / numPoints; sphere[1] = y / numPoints; sphere[2] = z / numPoints; let radius = 0; let dist; for (i = 0; i < numPoints; i++) { dist = Math.abs(math.lenVec3(math.subVec3(points[i], sphere, tempVec3))); if (dist > radius) { radius = dist; } } sphere[3] = radius; return sphere; }; }))(), /** * Finds the minimum boundary sphere enclosing the given 3D positions. * * @private */ positions3ToSphere3: ((() => { const tempVec3a = new FloatArrayType(3); const tempVec3b = new FloatArrayType(3); return (positions, sphere) => { sphere = sphere || math.vec4(); let x = 0; let y = 0; let z = 0; let i; const lenPositions = positions.length; let radius = 0; for (i = 0; i < lenPositions; i += 3) { x += positions[i]; y += positions[i + 1]; z += positions[i + 2]; } const numPositions = lenPositions / 3; sphere[0] = x / numPositions; sphere[1] = y / numPositions; sphere[2] = z / numPositions; let dist; for (i = 0; i < lenPositions; i += 3) { tempVec3a[0] = positions[i]; tempVec3a[1] = positions[i + 1]; tempVec3a[2] = positions[i + 2]; dist = Math.abs(math.lenVec3(math.subVec3(tempVec3a, sphere, tempVec3b))); if (dist > radius) { radius = dist; } } sphere[3] = radius; return sphere; }; }))(), /** * Finds the minimum boundary sphere enclosing the given 3D points. * * @private */ OBB3ToSphere3: ((() => { const point = new FloatArrayType(3); const tempVec3 = new FloatArrayType(3); return (points, sphere) => { sphere = sphere || math.vec4(); let x = 0; let y = 0; let z = 0; let i; const lenPoints = points.length; const numPoints = lenPoints / 4; for (i = 0; i < lenPoints; i += 4) { x += points[i + 0]; y += points[i + 1]; z += points[i + 2]; } sphere[0] = x / numPoints; sphere[1] = y / numPoints; sphere[2] = z / numPoints; let radius = 0; let dist; for (i = 0; i < lenPoints; i += 4) { point[0] = points[i + 0]; point[1] = points[i + 1]; point[2] = points[i + 2]; dist = Math.abs(math.lenVec3(math.subVec3(point, sphere, tempVec3))); if (dist > radius) { radius = dist; } } sphere[3] = radius; return sphere; }; }))(), /** * Gets the center of a bounding sphere. * * @private */ getSphere3Center(sphere, dest = math.vec3()) { dest[0] = sphere[0]; dest[1] = sphere[1]; dest[2] = sphere[2]; return dest; }, /** * Gets the 3D center of the given flat array of 3D positions. * * @private */ getPositionsCenter(positions, center = math.vec3()) { let xCenter = 0; let yCenter = 0; let zCenter = 0; for (var i = 0, len = positions.length; i < len; i += 3) { xCenter += positions[i + 0]; yCenter += positions[i + 1]; zCenter += positions[i + 2]; } const numPositions = positions.length / 3; center[0] = xCenter / numPositions; center[1] = yCenter / numPositions; center[2] = zCenter / numPositions; return center; }, /** * Expands the first axis-aligned 3D boundary to enclose the second, if required. * * @private */ expandAABB3(aabb1, aabb2) { if (aabb1[0] > aabb2[0]) { aabb1[0] = aabb2[0]; } if (aabb1[1] > aabb2[1]) { aabb1[1] = aabb2[1]; } if (aabb1[2] > aabb2[2]) { aabb1[2] = aabb2[2]; } if (aabb1[3] < aabb2[3]) { aabb1[3] = aabb2[3]; } if (aabb1[4] < aabb2[4]) { aabb1[4] = aabb2[4]; } if (aabb1[5] < aabb2[5]) { aabb1[5] = aabb2[5]; } return aabb1; }, /** * Expands an axis-aligned 3D boundary to enclose the given point, if needed. * * @private */ expandAABB3Point3(aabb, p) { if (aabb[0] > p[0]) { aabb[0] = p[0]; } if (aabb[1] > p[1]) { aabb[1] = p[1]; } if (aabb[2] > p[2]) { aabb[2] = p[2]; } if (aabb[3] < p[0]) { aabb[3] = p[0]; } if (aabb[4] < p[1]) { aabb[4] = p[1]; } if (aabb[5] < p[2]) { aabb[5] = p[2]; } return aabb; }, /** * Expands an axis-aligned 3D boundary to enclose the given points, if needed. * * @private */ expandAABB3Points3(aabb, positions) { var x; var y; var z; for (var i = 0, len = positions.length; i < len; i += 3) { x = positions[i]; y = positions[i + 1]; z = positions[i + 2]; if (aabb[0] > x) { aabb[0] = x; } if (aabb[1] > y) { aabb[1] = y; } if (aabb[2] > z) { aabb[2] = z; } if (aabb[3] < x) { aabb[3] = x; } if (aabb[4] < y) { aabb[4] = y; } if (aabb[5] < z) { aabb[5] = z; } } return aabb; }, /** * Collapses a 2D axis-aligned boundary, ready to expand to fit 2D points. * Creates new AABB if none supplied. * * @private */ collapseAABB2(aabb = math.AABB2()) { aabb[0] = math.MAX_DOUBLE; aabb[1] = math.MAX_DOUBLE; aabb[2] = math.MIN_DOUBLE; aabb[3] = math.MIN_DOUBLE; return aabb; }, point3AABB3Intersect(aabb, p) { return aabb[0] > p[0] || aabb[3] < p[0] || aabb[1] > p[1] || aabb[4] < p[1] || aabb[2] > p[2] || aabb[5] < p[2]; }, point3AABB3AbsoluteIntersect(aabb, p) { return ( aabb[0] <= p[0] && aabb[3] >= p[0] && aabb[1] <= p[1] && aabb[4] >= p[1] && aabb[2] <= p[2] && aabb[5] >= p[2] ); }, /** * * @param dir * @param constant * @param aabb * @returns {number} */ planeAABB3Intersect(dir, constant, aabb) { let min, max; if (dir[0] > 0) { min = dir[0] * aabb[0]; max = dir[0] * aabb[3]; } else { min = dir[0] * aabb[3]; max = dir[0] * aabb[0]; } if (dir[1] > 0) { min += dir[1] * aabb[1]; max += dir[1] * aabb[4]; } else { min += dir[1] * aabb[4]; max += dir[1] * aabb[1]; } if (dir[2] > 0) { min += dir[2] * aabb[2]; max += dir[2] * aabb[5]; } else { min += dir[2] * aabb[5]; max += dir[2] * aabb[2]; } const outside = (min <= -constant) && (max <= -constant); if (outside) { return -1; } const inside = (min >= -constant) && (max >= -constant); if (inside) { return 1; } return 0; }, /** * Finds the minimum 2D projected axis-aligned boundary enclosing the given 3D points. * * @private */ OBB3ToAABB2(points, aabb = math.AABB2()) { let xmin = math.MAX_DOUBLE; let ymin = math.MAX_DOUBLE; let xmax = math.MIN_DOUBLE; let ymax = math.MIN_DOUBLE; let x; let y; let w; let f; for (let i = 0, len = points.length; i < len; i += 4) { x = points[i + 0]; y = points[i + 1]; w = points[i + 3] || 1.0; f = 1.0 / w; x *= f; y *= f; if (x < xmin) { xmin = x; } if (y < ymin) { ymin = y; } if (x > xmax) { xmax = x; } if (y > ymax) { ymax = y; } } aabb[0] = xmin; aabb[1] = ymin; aabb[2] = xmax; aabb[3] = ymax; return aabb; }, /** * Expands the first axis-aligned 2D boundary to enclose the second, if required. * * @private */ expandAABB2(aabb1, aabb2) { if (aabb1[0] > aabb2[0]) { aabb1[0] = aabb2[0]; } if (aabb1[1] > aabb2[1]) { aabb1[1] = aabb2[1]; } if (aabb1[2] < aabb2[2]) { aabb1[2] = aabb2[2]; } if (aabb1[3] < aabb2[3]) { aabb1[3] = aabb2[3]; } return aabb1; }, /** * Expands an axis-aligned 2D boundary to enclose the given point, if required. * * @private */ expandAABB2Point2(aabb, p) { if (aabb[0] > p[0]) { aabb[0] = p[0]; } if (aabb[1] > p[1]) { aabb[1] = p[1]; } if (aabb[2] < p[0]) { aabb[2] = p[0]; } if (aabb[3] < p[1]) { aabb[3] = p[1]; } return aabb; }, AABB2ToCanvas(aabb, canvasWidth, canvasHeight, aabb2 = aabb) { const xmin = (aabb[0] + 1.0) * 0.5; const ymin = (aabb[1] + 1.0) * 0.5; const xmax = (aabb[2] + 1.0) * 0.5; const ymax = (aabb[3] + 1.0) * 0.5; aabb2[0] = Math.floor(xmin * canvasWidth); aabb2[1] = canvasHeight - Math.floor(ymax * canvasHeight); aabb2[2] = Math.floor(xmax * canvasWidth); aabb2[3] = canvasHeight - Math.floor(ymin * canvasHeight); return aabb2; }, //------------------------------------------------------------------------------------------------------------------ // Curves //------------------------------------------------------------------------------------------------------------------ tangentQuadraticBezier(t, p0, p1, p2) { return 2 * (1 - t) * (p1 - p0) + 2 * t * (p2 - p1); }, tangentQuadraticBezier3(t, p0, p1, p2, p3) { return -3 * p0 * (1 - t) * (1 - t) + 3 * p1 * (1 - t) * (1 - t) - 6 * t * p1 * (1 - t) + 6 * t * p2 * (1 - t) - 3 * t * t * p2 + 3 * t * t * p3; }, tangentSpline(t) { const h00 = 6 * t * t - 6 * t; const h10 = 3 * t * t - 4 * t + 1; const h01 = -6 * t * t + 6 * t; const h11 = 3 * t * t - 2 * t; return h00 + h10 + h01 + h11; }, catmullRomInterpolate(p0, p1, p2, p3, t) { const v0 = (p2 - p0) * 0.5; const v1 = (p3 - p1) * 0.5; const t2 = t * t; const t3 = t * t2; return (2 * p1 - 2 * p2 + v0 + v1) * t3 + (-3 * p1 + 3 * p2 - 2 * v0 - v1) * t2 + v0 * t + p1; }, // Bezier Curve formulii from http://en.wikipedia.org/wiki/B%C3%A9zier_curve // Quad Bezier Functions b2p0(t, p) { const k = 1 - t; return k * k * p; }, b2p1(t, p) { return 2 * (1 - t) * t * p; }, b2p2(t, p) { return t * t * p; }, b2(t, p0, p1, p2) { return this.b2p0(t, p0) + this.b2p1(t, p1) + this.b2p2(t, p2); }, // Cubic Bezier Functions b3p0(t, p) { const k = 1 - t; return k * k * k * p; }, b3p1(t, p) { const k = 1 - t; return 3 * k * k * t * p; }, b3p2(t, p) { const k = 1 - t; return 3 * k * t * t * p; }, b3p3(t, p) { return t * t * t * p; }, b3(t, p0, p1, p2, p3) { return this.b3p0(t, p0) + this.b3p1(t, p1) + this.b3p2(t, p2) + this.b3p3(t, p3); }, //------------------------------------------------------------------------------------------------------------------ // Geometry //------------------------------------------------------------------------------------------------------------------ /** * Calculates the normal vector of a triangle. * * @private */ triangleNormal(a, b, c, normal = math.vec3()) { const p1x = b[0] - a[0]; const p1y = b[1] - a[1]; const p1z = b[2] - a[2]; const p2x = c[0] - a[0]; const p2y = c[1] - a[1]; const p2z = c[2] - a[2]; const p3x = p1y * p2z - p1z * p2y; const p3y = p1z * p2x - p1x * p2z; const p3z = p1x * p2y - p1y * p2x; const mag = Math.sqrt(p3x * p3x + p3y * p3y + p3z * p3z); if (mag === 0) { normal[0] = 0; normal[1] = 0; normal[2] = 0; } else { normal[0] = p3x / mag; normal[1] = p3y / mag; normal[2] = p3z / mag; } return normal }, /** * Finds the intersection of a 3D ray with a 3D triangle. * * @private */ rayTriangleIntersect: ((() => { const tempVec3 = new FloatArrayType(3); const tempVec3b = new FloatArrayType(3); const tempVec3c = new FloatArrayType(3); const tempVec3d = new FloatArrayType(3); const tempVec3e = new FloatArrayType(3); return (origin, dir, a, b, c, isect) => { isect = isect || math.vec3(); const EPSILON = 0.000001; const edge1 = math.subVec3(b, a, tempVec3); const edge2 = math.subVec3(c, a, tempVec3b); const pvec = math.cross3Vec3(dir, edge2, tempVec3c); const det = math.dotVec3(edge1, pvec); if (det < EPSILON) { return null; } const tvec = math.subVec3(origin, a, tempVec3d); const u = math.dotVec3(tvec, pvec); if (u < 0 || u > det) { return null; } const qvec = math.cross3Vec3(tvec, edge1, tempVec3e); const v = math.dotVec3(dir, qvec); if (v < 0 || u + v > det) { return null; } const t = math.dotVec3(edge2, qvec) / det; isect[0] = origin[0] + t * dir[0]; isect[1] = origin[1] + t * dir[1]; isect[2] = origin[2] + t * dir[2]; return isect; }; }))(), /** * Finds the intersection of a 3D ray with a plane defined by 3 points. * * @private */ rayPlaneIntersect: ((() => { const tempVec3 = new FloatArrayType(3); const tempVec3b = new FloatArrayType(3); const tempVec3c = new FloatArrayType(3); const tempVec3d = new FloatArrayType(3); return (origin, dir, a, b, c, isect) => { isect = isect || math.vec3(); dir = math.normalizeVec3(dir, tempVec3); const edge1 = math.subVec3(b, a, tempVec3b); const edge2 = math.subVec3(c, a, tempVec3c); const n = math.cross3Vec3(edge1, edge2, tempVec3d); math.normalizeVec3(n, n); const d = -math.dotVec3(a, n); const t = -(math.dotVec3(origin, n) + d) / math.dotVec3(dir, n); isect[0] = origin[0] + t * dir[0]; isect[1] = origin[1] + t * dir[1]; isect[2] = origin[2] + t * dir[2]; return isect; }; }))(), /** * Gets barycentric coordinates from cartesian coordinates within a triangle. * Gets barycentric coordinates from cartesian coordinates within a triangle. * * @private */ cartesianToBarycentric: ((() => { const tempVec3 = new FloatArrayType(3); const tempVec3b = new FloatArrayType(3); const tempVec3c = new FloatArrayType(3); return (cartesian, a, b, c, dest) => { const v0 = math.subVec3(c, a, tempVec3); const v1 = math.subVec3(b, a, tempVec3b); const v2 = math.subVec3(cartesian, a, tempVec3c); const dot00 = math.dotVec3(v0, v0); const dot01 = math.dotVec3(v0, v1); const dot02 = math.dotVec3(v0, v2); const dot11 = math.dotVec3(v1, v1); const dot12 = math.dotVec3(v1, v2); const denom = (dot00 * dot11 - dot01 * dot01); // Colinear or singular triangle if (denom === 0) { // Arbitrary location outside of triangle return null; } const invDenom = 1 / denom; const u = (dot11 * dot02 - dot01 * dot12) * invDenom; const v = (dot00 * dot12 - dot01 * dot02) * invDenom; dest[0] = 1 - u - v; dest[1] = v; dest[2] = u; return dest; }; }))(), /** * Returns true if the given barycentric coordinates are within their triangle. * * @private */ barycentricInsideTriangle(bary) { const v = bary[1]; const u = bary[2]; return (u >= 0) && (v >= 0) && (u + v < 1); }, /** * Gets cartesian coordinates from barycentric coordinates within a triangle. * * @private */ barycentricToCartesian(bary, a, b, c, cartesian = math.vec3()) { const u = bary[0]; const v = bary[1]; const w = bary[2]; cartesian[0] = a[0] * u + b[0] * v + c[0] * w; cartesian[1] = a[1] * u + b[1] * v + c[1] * w; cartesian[2] = a[2] * u + b[2] * v + c[2] * w; return cartesian; }, /** * Given geometry defined as an array of positions, optional normals, option uv and an array of indices, returns * modified arrays that have duplicate vertices removed. * * Note: does not work well when co-incident vertices have same positions but different normals and UVs. * * @param positions * @param normals * @param uv * @param indices * @returns {{positions: Array, indices: Array}} * @private */ mergeVertices(positions, normals, uv, indices) { const positionsMap = {}; // Hashmap for looking up vertices by position coordinates (and making sure they are unique) const indicesLookup = []; const uniquePositions = []; const uniqueNormals = normals ? [] : null; const uniqueUV = uv ? [] : null; const indices2 = []; let vx; let vy; let vz; let key; const precisionPoints = 4; // number of decimal points, e.g. 4 for epsilon of 0.0001 const precision = 10 ** precisionPoints; let i; let len; let uvi = 0; for (i = 0, len = positions.length; i < len; i += 3) { vx = positions[i]; vy = positions[i + 1]; vz = positions[i + 2]; key = `${Math.round(vx * precision)}_${Math.round(vy * precision)}_${Math.round(vz * precision)}`; if (positionsMap[key] === undefined) { positionsMap[key] = uniquePositions.length / 3; uniquePositions.push(vx); uniquePositions.push(vy); uniquePositions.push(vz); if (normals) { uniqueNormals.push(normals[i]); uniqueNormals.push(normals[i + 1]); uniqueNormals.push(normals[i + 2]); } if (uv) { uniqueUV.push(uv[uvi]); uniqueUV.push(uv[uvi + 1]); } } indicesLookup[i / 3] = positionsMap[key]; uvi += 2; } for (i = 0, len = indices.length; i < len; i++) { indices2[i] = indicesLookup[indices[i]]; } const result = { positions: uniquePositions, indices: indices2 }; if (uniqueNormals) { result.normals = uniqueNormals; } if (uniqueUV) { result.uv = uniqueUV; } return result; }, /** * Builds normal vectors from positions and indices. * * @private */ buildNormals: ((() => { const a = new FloatArrayType(3); const b = new FloatArrayType(3); const c = new FloatArrayType(3); const ab = new FloatArrayType(3); const ac = new FloatArrayType(3); const crossVec = new FloatArrayType(3); return (positions, indices, normals) => { let i; let len; const nvecs = new Array(positions.length / 3); let j0; let j1; let j2; for (i = 0, len = indices.length; i < len; i += 3) { j0 = indices[i]; j1 = indices[i + 1]; j2 = indices[i + 2]; a[0] = positions[j0 * 3]; a[1] = positions[j0 * 3 + 1]; a[2] = positions[j0 * 3 + 2]; b[0] = positions[j1 * 3]; b[1] = positions[j1 * 3 + 1]; b[2] = positions[j1 * 3 + 2]; c[0] = positions[j2 * 3]; c[1] = positions[j2 * 3 + 1]; c[2] = positions[j2 * 3 + 2]; math.subVec3(b, a, ab); math.subVec3(c, a, ac); const normVec = math.vec3(); math.normalizeVec3(math.cross3Vec3(ab, ac, crossVec), normVec); if (!nvecs[j0]) { nvecs[j0] = []; } if (!nvecs[j1]) { nvecs[j1] = []; } if (!nvecs[j2]) { nvecs[j2] = []; } nvecs[j0].push(normVec); nvecs[j1].push(normVec); nvecs[j2].push(normVec); } normals = (normals && normals.length === positions.length) ? normals : new Float32Array(positions.length); let count; let x; let y; let z; for (i = 0, len = nvecs.length; i < len; i++) { // Now go through and average out everything count = nvecs[i].length; x = 0; y = 0; z = 0; for (let j = 0; j < count; j++) { x += nvecs[i][j][0]; y += nvecs[i][j][1]; z += nvecs[i][j][2]; } normals[i * 3] = (x / count); normals[i * 3 + 1] = (y / count); normals[i * 3 + 2] = (z / count); } return normals; }; }))(), /** * Builds vertex tangent vectors from positions, UVs and indices. * * @private */ buildTangents: ((() => { const tempVec3 = new FloatArrayType(3); const tempVec3b = new FloatArrayType(3); const tempVec3c = new FloatArrayType(3); const tempVec3d = new FloatArrayType(3); const tempVec3e = new FloatArrayType(3); const tempVec3f = new FloatArrayType(3); const tempVec3g = new FloatArrayType(3); return (positions, indices, uv) => { const tangents = new Float32Array(positions.length); // The vertex arrays needs to be calculated // before the calculation of the tangents for (let location = 0; location < indices.length; location += 3) { // Recontructing each vertex and UV coordinate into the respective vectors let index = indices[location]; const v0 = positions.subarray(index * 3, index * 3 + 3); const uv0 = uv.subarray(index * 2, index * 2 + 2); index = indices[location + 1]; const v1 = positions.subarray(index * 3, index * 3 + 3); const uv1 = uv.subarray(index * 2, index * 2 + 2); index = indices[location + 2]; const v2 = positions.subarray(index * 3, index * 3 + 3); const uv2 = uv.subarray(index * 2, index * 2 + 2); const deltaPos1 = math.subVec3(v1, v0, tempVec3); const deltaPos2 = math.subVec3(v2, v0, tempVec3b); const deltaUV1 = math.subVec2(uv1, uv0, tempVec3c); const deltaUV2 = math.subVec2(uv2, uv0, tempVec3d); const r = 1 / ((deltaUV1[0] * deltaUV2[1]) - (deltaUV1[1] * deltaUV2[0])); const tangent = math.mulVec3Scalar( math.subVec3( math.mulVec3Scalar(deltaPos1, deltaUV2[1], tempVec3e), math.mulVec3Scalar(deltaPos2, deltaUV1[1], tempVec3f), tempVec3g ), r, tempVec3f ); // Average the value of the vectors let addTo; for (let v = 0; v < 3; v++) { addTo = indices[location + v] * 3; tangents[addTo] += tangent[0]; tangents[addTo + 1] += tangent[1]; tangents[addTo + 2] += tangent[2]; } } return tangents; }; }))(), /** * Builds vertex and index arrays needed by color-indexed triangle picking. * * @private */ buildPickTriangles(positions, indices, compressGeometry) { const numIndices = indices.length; const pickPositions = compressGeometry ? new Uint16Array(numIndices * 9) : new Float32Array(numIndices * 9); const pickColors = new Uint8Array(numIndices * 12); let primIndex = 0; let vi;// Positions array index let pvi = 0;// Picking positions array index let pci = 0; // Picking color array index // Triangle indices let i; let r; let g; let b; let a; for (let location = 0; location < numIndices; location += 3) { // Primitive-indexed triangle pick color a = (primIndex >> 24 & 0xFF); b = (primIndex >> 16 & 0xFF); g = (primIndex >> 8 & 0xFF); r = (primIndex & 0xFF); // A i = indices[location]; vi = i * 3; pickPositions[pvi++] = positions[vi]; pickPositions[pvi++] = positions[vi + 1]; pickPositions[pvi++] = positions[vi + 2]; pickColors[pci++] = r; pickColors[pci++] = g; pickColors[pci++] = b; pickColors[pci++] = a; // B i = indices[location + 1]; vi = i * 3; pickPositions[pvi++] = positions[vi]; pickPositions[pvi++] = positions[vi + 1]; pickPositions[pvi++] = positions[vi + 2]; pickColors[pci++] = r; pickColors[pci++] = g; pickColors[pci++] = b; pickColors[pci++] = a; // C i = indices[location + 2]; vi = i * 3; pickPositions[pvi++] = positions[vi]; pickPositions[pvi++] = positions[vi + 1]; pickPositions[pvi++] = positions[vi + 2]; pickColors[pci++] = r; pickColors[pci++] = g; pickColors[pci++] = b; pickColors[pci++] = a; primIndex++; } return { positions: pickPositions, colors: pickColors }; }, /** * Converts surface-perpendicular face normals to vertex normals. Assumes that the mesh contains disjoint triangles * that don't share vertex array elements. Works by finding groups of vertices that have the same location and * averaging their normal vectors. * * @returns {{positions: Array, normals: *}} */ faceToVertexNormals(positions, normals, options = {}) { const smoothNormalsAngleThreshold = options.smoothNormalsAngleThreshold || 20; const vertexMap = {}; const vertexNormals = []; const vertexNormalAccum = {}; let acc; let vx; let vy; let vz; let key; const precisionPoints = 4; // number of decimal points, e.g. 4 for epsilon of 0.0001 const precision = 10 ** precisionPoints; let posi; let i; let j; let len; let a; let b; for (i = 0, len = positions.length; i < len; i += 3) { posi = i / 3; vx = positions[i]; vy = positions[i + 1]; vz = positions[i + 2]; key = `${Math.round(vx * precision)}_${Math.round(vy * precision)}_${Math.round(vz * precision)}`; if (vertexMap[key] === undefined) { vertexMap[key] = [posi]; } else { vertexMap[key].push(posi); } const normal = math.normalizeVec3([normals[i], normals[i + 1], normals[i + 2]]); vertexNormals[posi] = normal; acc = math.vec4([normal[0], normal[1], normal[2], 1]); vertexNormalAccum[posi] = acc; } for (key in vertexMap) { if (vertexMap.hasOwnProperty(key)) { const vertices = vertexMap[key]; const numVerts = vertices.length; for (i = 0; i < numVerts; i++) { const ii = vertices[i]; acc = vertexNormalAccum[ii]; for (j = 0; j < numVerts; j++) { if (i === j) { continue; } const jj = vertices[j]; a = vertexNormals[ii]; b = vertexNormals[jj]; const angle = Math.abs(math.angleVec3(a, b) / math.DEGTORAD); if (angle < smoothNormalsAngleThreshold) { acc[0] += b[0]; acc[1] += b[1]; acc[2] += b[2]; acc[3] += 1.0; } } } } } for (i = 0, len = normals.length; i < len; i += 3) { acc = vertexNormalAccum[i / 3]; normals[i + 0] = acc[0] / acc[3]; normals[i + 1] = acc[1] / acc[3]; normals[i + 2] = acc[2] / acc[3]; } }, //------------------------------------------------------------------------------------------------------------------ // Ray casting //------------------------------------------------------------------------------------------------------------------ /** Transforms a ray by a matrix. @method transformRay @static @param {Number[]} matrix 4x4 matrix @param {Number[]} rayOrigin The ray origin @param {Number[]} rayDir The ray direction @param {Number[]} rayOriginDest The transformed ray origin @param {Number[]} rayDirDest The transformed ray direction */ transformRay: ((() => { const tempVec4a = new FloatArrayType(4); const tempVec4b = new FloatArrayType(4); return (matrix, rayOrigin, rayDir, rayOriginDest, rayDirDest) => { tempVec4a[0] = rayOrigin[0]; tempVec4a[1] = rayOrigin[1]; tempVec4a[2] = rayOrigin[2]; tempVec4a[3] = 1; math.transformVec4(matrix, tempVec4a, tempVec4b); rayOriginDest[0] = tempVec4b[0]; rayOriginDest[1] = tempVec4b[1]; rayOriginDest[2] = tempVec4b[2]; tempVec4a[0] = rayDir[0]; tempVec4a[1] = rayDir[1]; tempVec4a[2] = rayDir[2]; math.transformVec3(matrix, tempVec4a, tempVec4b); math.normalizeVec3(tempVec4b); rayDirDest[0] = tempVec4b[0]; rayDirDest[1] = tempVec4b[1]; rayDirDest[2] = tempVec4b[2]; }; }))(), /** Transforms a Canvas-space position into a World-space ray, in the context of a Camera. @method canvasPosToWorldRay @static @param {Number[]} viewMatrix View matrix @param {Number[]} projMatrix Projection matrix @param {String} projection Projection type (e.g. "ortho") @param {Number[]} canvasPos The Canvas-space position. @param {Number[]} worldRayOrigin The World-space ray origin. @param {Number[]} worldRayDir The World-space ray direction. */ canvasPosToWorldRay: ((() => { const pvMatInv = new FloatArrayType(16); const vec4Near = new FloatArrayType(4); const vec4Far = new FloatArrayType(4); const clipToWorld = (clipX, clipY, clipZ, isOrtho, outVec4) => { outVec4[0] = clipX; outVec4[1] = clipY; outVec4[2] = clipZ; outVec4[3] = 1; math.transformVec4(pvMatInv, outVec4, outVec4); if (! isOrtho) math.mulVec4Scalar(outVec4, 1 / outVec4[3]); }; return (canvas, viewMatrix, projMatrix, projection, canvasPos, worldRayOrigin, worldRayDir) => { const isOrtho = projection === "ortho"; math.mulMat4(projMatrix, viewMatrix, pvMatInv); math.inverseMat4(pvMatInv, pvMatInv); // Calculate clip space coordinates, which will be in range // of x=[-1..1] and y=[-1..1], with y=(+1) at top // clientWidth/Height needs to be used in case canvas.width/height is scaled down, // but not reflecting client dimensions (e.g. when using FastNavPlugin) const clipX = 2 * canvasPos[0] / canvas.clientWidth - 1; // Calculate clip space coordinates const clipY = 1 - 2 * canvasPos[1] / canvas.clientHeight; clipToWorld(clipX, clipY, -1, isOrtho, vec4Near); clipToWorld(clipX, clipY, 1, isOrtho, vec4Far); worldRayOrigin[0] = vec4Near[0]; worldRayOrigin[1] = vec4Near[1]; worldRayOrigin[2] = vec4Near[2]; math.subVec3(vec4Far, vec4Near, worldRayDir); math.normalizeVec3(worldRayDir); }; }))(), /** Transforms a Canvas-space position to a Mesh's Local-space coordinate system, in the context of a Camera. @method canvasPosToLocalRay @static @param {Camera} camera The Camera. @param {Mesh} mesh The Mesh. @param {Number[]} viewMatrix View matrix @param {Number[]} projMatrix Projection matrix @param {Number[]} worldMatrix Modeling matrix @param {Number[]} canvasPos The Canvas-space position. @param {Number[]} localRayOrigin The Local-space ray origin. @param {Number[]} localRayDir The Local-space ray direction. */ canvasPosToLocalRay: ((() => { const worldRayOrigin = new FloatArrayType(3); const worldRayDir = new FloatArrayType(3); return (canvas, viewMatrix, projMatrix, projection, worldMatrix, canvasPos, localRayOrigin, localRayDir) => { math.canvasPosToWorldRay(canvas, viewMatrix, projMatrix, projection, canvasPos, worldRayOrigin, worldRayDir); math.worldRayToLocalRay(worldMatrix, worldRayOrigin, worldRayDir, localRayOrigin, localRayDir); }; }))(), /** Transforms a ray from World-space to a Mesh's Local-space coordinate system. @method worldRayToLocalRay @static @param {Number[]} worldMatrix The World transform matrix @param {Number[]} worldRayOrigin The World-space ray origin. @param {Number[]} worldRayDir The World-space ray direction. @param {Number[]} localRayOrigin The Local-space ray origin. @param {Number[]} localRayDir The Local-space ray direction. */ worldRayToLocalRay: ((() => { const tempMat4 = new FloatArrayType(16); const tempVec4a = new FloatArrayType(4); const tempVec4b = new FloatArrayType(4); return (worldMatrix, worldRayOrigin, worldRayDir, localRayOrigin, localRayDir) => { const modelMatInverse = math.inverseMat4(worldMatrix, tempMat4); tempVec4a[0] = worldRayOrigin[0]; tempVec4a[1] = worldRayOrigin[1]; tempVec4a[2] = worldRayOrigin[2]; tempVec4a[3] = 1; math.transformVec4(modelMatInverse, tempVec4a, tempVec4b); localRayOrigin[0] = tempVec4b[0]; localRayOrigin[1] = tempVec4b[1]; localRayOrigin[2] = tempVec4b[2]; math.transformVec3(modelMatInverse, worldRayDir, localRayDir); }; }))(), buildKDTree: ((() => { const KD_TREE_MAX_DEPTH = 10; const KD_TREE_MIN_TRIANGLES = 20; const dimLength = new Float32Array(); function buildNode(triangles, indices, positions, depth) { const aabb = new FloatArrayType(6); const node = { triangles: null, left: null, right: null, leaf: false, splitDim: 0, aabb }; aabb[0] = aabb[1] = aabb[2] = Number.POSITIVE_INFINITY; aabb[3] = aabb[4] = aabb[5] = Number.NEGATIVE_INFINITY; let t; let len; for (t = 0, len = triangles.length; t < len; ++t) { var ii = triangles[t] * 3; for (let j = 0; j < 3; ++j) { const pi = indices[ii + j] * 3; if (positions[pi] < aabb[0]) { aabb[0] = positions[pi]; } if (positions[pi] > aabb[3]) { aabb[3] = positions[pi]; } if (positions[pi + 1] < aabb[1]) { aabb[1] = positions[pi + 1]; } if (positions[pi + 1] > aabb[4]) { aabb[4] = positions[pi + 1]; } if (positions[pi + 2] < aabb[2]) { aabb[2] = positions[pi + 2]; } if (positions[pi + 2] > aabb[5]) { aabb[5] = positions[pi + 2]; } } } if (triangles.length < KD_TREE_MIN_TRIANGLES || depth > KD_TREE_MAX_DEPTH) { node.triangles = triangles; node.leaf = true; return node; } dimLength[0] = aabb[3] - aabb[0]; dimLength[1] = aabb[4] - aabb[1]; dimLength[2] = aabb[5] - aabb[2]; let dim = 0; if (dimLength[1] > dimLength[dim]) { dim = 1; } if (dimLength[2] > dimLength[dim]) { dim = 2; } node.splitDim = dim; const mid = (aabb[dim] + aabb[dim + 3]) / 2; const left = new Array(triangles.length); let numLeft = 0; const right = new Array(triangles.length); let numRight = 0; for (t = 0, len = triangles.length; t < len; ++t) { var ii = triangles[t] * 3; const i0 = indices[ii]; const i1 = indices[ii + 1]; const i2 = indices[ii + 2]; const pi0 = i0 * 3; const pi1 = i1 * 3; const pi2 = i2 * 3; if (positions[pi0 + dim] <= mid || positions[pi1 + dim] <= mid || positions[pi2 + dim] <= mid) { left[numLeft++] = triangles[t]; } else { right[numRight++] = triangles[t]; } } left.length = numLeft; right.length = numRight; node.left = buildNode(left, indices, positions, depth + 1); node.right = buildNode(right, indices, positions, depth + 1); return node; } return (indices, positions) => { const numTris = indices.length / 3; const triangles = new Array(numTris); for (let i = 0; i < numTris; ++i) { triangles[i] = i; } return buildNode(triangles, indices, positions, 0); }; }))(), decompressPosition(position, decodeMatrix, dest) { dest = dest || position; dest[0] = position[0] * decodeMatrix[0] + decodeMatrix[12]; dest[1] = position[1] * decodeMatrix[5] + decodeMatrix[13]; dest[2] = position[2] * decodeMatrix[10] + decodeMatrix[14]; }, decompressPositions(positions, decodeMatrix, dest = new Float32Array(positions.length)) { for (let i = 0, len = positions.length; i < len; i += 3) { dest[i + 0] = positions[i + 0] * decodeMatrix[0] + decodeMatrix[12]; dest[i + 1] = positions[i + 1] * decodeMatrix[5] + decodeMatrix[13]; dest[i + 2] = positions[i + 2] * decodeMatrix[10] + decodeMatrix[14]; } return dest; }, decompressUV(uv, decodeMatrix, dest) { dest[0] = uv[0] * decodeMatrix[0] + decodeMatrix[6]; dest[1] = uv[1] * decodeMatrix[4] + decodeMatrix[7]; }, decompressUVs(uvs, decodeMatrix, dest = new Float32Array(uvs.length)) { for (let i = 0, len = uvs.length; i < len; i += 3) { dest[i + 0] = uvs[i + 0] * decodeMatrix[0] + decodeMatrix[6]; dest[i + 1] = uvs[i + 1] * decodeMatrix[4] + decodeMatrix[7]; } return dest; }, octDecodeVec2(oct, result) { let x = oct[0]; let y = oct[1]; x = (2 * x + 1) / 255; y = (2 * y + 1) / 255; const z = 1 - Math.abs(x) - Math.abs(y); if (z < 0) { x = (1 - Math.abs(y)) * (x >= 0 ? 1 : -1); y = (1 - Math.abs(x)) * (y >= 0 ? 1 : -1); } const length = Math.sqrt(x * x + y * y + z * z); result[0] = x / length; result[1] = y / length; result[2] = z / length; return result; }, octDecodeVec2s(octs, result) { for (let i = 0, j = 0, len = octs.length; i < len; i += 2) { let x = octs[i + 0]; let y = octs[i + 1]; x = (2 * x + 1) / 255; y = (2 * y + 1) / 255; const z = 1 - Math.abs(x) - Math.abs(y); if (z < 0) { x = (1 - Math.abs(y)) * (x >= 0 ? 1 : -1); y = (1 - Math.abs(x)) * (y >= 0 ? 1 : -1); } const length = Math.sqrt(x * x + y * y + z * z); result[j + 0] = x / length; result[j + 1] = y / length; result[j + 2] = z / length; j += 3; } return result; } }; math.buildEdgeIndices = (function () { const uniquePositions = []; const indicesLookup = []; const indicesReverseLookup = []; const weldedIndices = []; // TODO: Optimize with caching, but need to cater to both compressed and uncompressed positions const faces = []; let numFaces = 0; const compa = new Uint16Array(3); const compb = new Uint16Array(3); const compc = new Uint16Array(3); const a = math.vec3(); const b = math.vec3(); const c = math.vec3(); const cb = math.vec3(); const ab = math.vec3(); const cross = math.vec3(); const normal = math.vec3(); function weldVertices(positions, indices) { const positionsMap = {}; // Hashmap for looking up vertices by position coordinates (and making sure they are unique) let vx; let vy; let vz; let key; const precisionPoints = 4; // number of decimal points, e.g. 4 for epsilon of 0.0001 const precision = Math.pow(10, precisionPoints); let i; let len; let lenUniquePositions = 0; for (i = 0, len = positions.length; i < len; i += 3) { vx = positions[i]; vy = positions[i + 1]; vz = positions[i + 2]; key = Math.round(vx * precision) + '_' + Math.round(vy * precision) + '_' + Math.round(vz * precision); if (positionsMap[key] === undefined) { positionsMap[key] = lenUniquePositions / 3; uniquePositions[lenUniquePositions++] = vx; uniquePositions[lenUniquePositions++] = vy; uniquePositions[lenUniquePositions++] = vz; } indicesLookup[i / 3] = positionsMap[key]; } for (i = 0, len = indices.length; i < len; i++) { weldedIndices[i] = indicesLookup[indices[i]]; indicesReverseLookup[weldedIndices[i]] = indices[i]; } } function buildFaces(numIndices, positionsDecodeMatrix) { numFaces = 0; for (let i = 0, len = numIndices; i < len; i += 3) { const ia = ((weldedIndices[i]) * 3); const ib = ((weldedIndices[i + 1]) * 3); const ic = ((weldedIndices[i + 2]) * 3); if (positionsDecodeMatrix) { compa[0] = uniquePositions[ia]; compa[1] = uniquePositions[ia + 1]; compa[2] = uniquePositions[ia + 2]; compb[0] = uniquePositions[ib]; compb[1] = uniquePositions[ib + 1]; compb[2] = uniquePositions[ib + 2]; compc[0] = uniquePositions[ic]; compc[1] = uniquePositions[ic + 1]; compc[2] = uniquePositions[ic + 2]; // Decode math.decompressPosition(compa, positionsDecodeMatrix, a); math.decompressPosition(compb, positionsDecodeMatrix, b); math.decompressPosition(compc, positionsDecodeMatrix, c); } else { a[0] = uniquePositions[ia]; a[1] = uniquePositions[ia + 1]; a[2] = uniquePositions[ia + 2]; b[0] = uniquePositions[ib]; b[1] = uniquePositions[ib + 1]; b[2] = uniquePositions[ib + 2]; c[0] = uniquePositions[ic]; c[1] = uniquePositions[ic + 1]; c[2] = uniquePositions[ic + 2]; } math.subVec3(c, b, cb); math.subVec3(a, b, ab); math.cross3Vec3(cb, ab, cross); math.normalizeVec3(cross, normal); const face = faces[numFaces] || (faces[numFaces] = {normal: math.vec3()}); face.normal[0] = normal[0]; face.normal[1] = normal[1]; face.normal[2] = normal[2]; numFaces++; } } return function (positions, indices, positionsDecodeMatrix, edgeThreshold) { weldVertices(positions, indices); buildFaces(indices.length, positionsDecodeMatrix); const edgeIndices = []; const thresholdDot = Math.cos(math.DEGTORAD * edgeThreshold); const edges = {}; let edge1; let edge2; let index1; let index2; let key; let largeIndex = false; let edge; let normal1; let normal2; let dot; let ia; let ib; for (let i = 0, len = indices.length; i < len; i += 3) { const faceIndex = i / 3; for (let j = 0; j < 3; j++) { edge1 = weldedIndices[i + j]; edge2 = weldedIndices[i + ((j + 1) % 3)]; index1 = Math.min(edge1, edge2); index2 = Math.max(edge1, edge2); key = index1 + "," + index2; if (edges[key] === undefined) { edges[key] = { index1: index1, index2: index2, face1: faceIndex, face2: undefined }; } else { edges[key].face2 = faceIndex; } } } for (key in edges) { edge = edges[key]; // an edge is only rendered if the angle (in degrees) between the face normals of the adjoining faces exceeds this value. default = 1 degree. if (edge.face2 !== undefined) { normal1 = faces[edge.face1].normal; normal2 = faces[edge.face2].normal; dot = math.dotVec3(normal1, normal2); if (dot > thresholdDot) { continue; } } ia = indicesReverseLookup[edge.index1]; ib = indicesReverseLookup[edge.index2]; if (!largeIndex && ia > 65535 || ib > 65535) { largeIndex = true; } edgeIndices.push(ia); edgeIndices.push(ib); } return (largeIndex) ? new Uint32Array(edgeIndices) : new Uint16Array(edgeIndices); }; })(); /** * Returns `true` if a plane clips the given 3D positions. * @param {Number[]} pos Position in plane * @param {Number[]} dir Direction of plane * @param {number} positions Flat array of 3D positions. * @param {number} numElementsPerPosition Number of elements perposition - usually either 3 or 4. * @returns {boolean} */ math.planeClipsPositions3 = function (pos, dir, positions, numElementsPerPosition = 3) { for (let i = 0, len = positions.length; i < len; i += numElementsPerPosition) { tempVec3a$P[0] = positions[i + 0] - pos[0]; tempVec3a$P[1] = positions[i + 1] - pos[1]; tempVec3a$P[2] = positions[i + 2] - pos[2]; let dotProduct = tempVec3a$P[0] * dir[0] + tempVec3a$P[1] * dir[1] + tempVec3a$P[2] * dir[2]; if (dotProduct < 0) { return true; } } return false; }; const MAX_KD_TREE_DEPTH$1 = 15; // Increase if greater precision needed const kdTreeDimLength$1 = new Float32Array(3); /** * Automatically indexes a {@link Viewer}'s {@link Entity}s in a 3D k-d tree * to support fast collision detection with 3D World-space axis-aligned boundaries (AABBs) and frustums. * * See {@link MarqueePicker} for usage example. * * An ObjectsKdTree3 is configured with a Viewer, and will then automatically * keep itself populated with k-d nodes that contain the Viewer's Entitys. * * We can then traverse the k-d nodes, starting at {@link ObjectsKdTree3#root}, to find * the contained Entities. */ class ObjectsKdTree3 { /** * Creates an ObjectsKdTree3. * * @param {*} cfg Configuration * @param {Viewer} cfg.viewer The Viewer that provides the {@link Entity}s in this ObjectsKdTree3. * @param {number} [cfg.maxTreeDepth=15] Optional maximum depth for the k-d tree. */ constructor(cfg) { if (!cfg) { throw "Parameter expected: cfg"; } if (!cfg.viewer) { throw "Parameter expected: cfg.viewer"; } this.viewer = cfg.viewer; this._maxTreeDepth = cfg.maxTreeDepth || MAX_KD_TREE_DEPTH$1; this._root = null; this._needsRebuild = true; this._onModelLoaded = this.viewer.scene.on("modelLoaded", (modelId) => { this._needsRebuild = true; }); this._onModelUnloaded = this.viewer.scene.on("modelUnloaded", (modelId) => { this._needsRebuild = true; }); } /** * Gets the root ObjectsKdTree3 node. * * Each time this accessor is accessed, it will lazy-rebuild the ObjectsKdTree3 * if {@link Entity}s have been created or removed in the {@link Viewer} since the last time it was accessed. */ get root() { if (this._needsRebuild) { this._rebuild(); } return this._root; } _rebuild() { const viewer = this.viewer; const scene = viewer.scene; const depth = 0; this._root = { aabb: scene.getAABB() }; for (let objectId in scene.objects) { const entity = scene.objects[objectId]; this._insertEntity(this._root, entity, depth + 1); } this._needsRebuild = false; } _insertEntity(node, entity, depth) { const entityAABB = entity.aabb; if (depth >= this._maxTreeDepth) { node.entities = node.entities || []; node.entities.push(entity); return; } if (node.left) { if (math.containsAABB3(node.left.aabb, entityAABB)) { this._insertEntity(node.left, entity, depth + 1); return; } } if (node.right) { if (math.containsAABB3(node.right.aabb, entityAABB)) { this._insertEntity(node.right, entity, depth + 1); return; } } const nodeAABB = node.aabb; kdTreeDimLength$1[0] = nodeAABB[3] - nodeAABB[0]; kdTreeDimLength$1[1] = nodeAABB[4] - nodeAABB[1]; kdTreeDimLength$1[2] = nodeAABB[5] - nodeAABB[2]; let dim = 0; if (kdTreeDimLength$1[1] > kdTreeDimLength$1[dim]) { dim = 1; } if (kdTreeDimLength$1[2] > kdTreeDimLength$1[dim]) { dim = 2; } if (!node.left) { const aabbLeft = nodeAABB.slice(); aabbLeft[dim + 3] = ((nodeAABB[dim] + nodeAABB[dim + 3]) / 2.0); node.left = { aabb: aabbLeft }; if (math.containsAABB3(aabbLeft, entityAABB)) { this._insertEntity(node.left, entity, depth + 1); return; } } if (!node.right) { const aabbRight = nodeAABB.slice(); aabbRight[dim] = ((nodeAABB[dim] + nodeAABB[dim + 3]) / 2.0); node.right = { aabb: aabbRight }; if (math.containsAABB3(aabbRight, entityAABB)) { this._insertEntity(node.right, entity, depth + 1); return; } } node.entities = node.entities || []; node.entities.push(entity); } /** * Destroys this ObjectsKdTree3. * * Does not destroy the {@link Viewer} given to the constructor of the ObjectsKdTree3. */ destroy() { const scene = this.viewer.scene; scene.off(this._onModelLoaded); scene.off(this._onModelUnloaded); this._root = null; this._needsRebuild = true; } } // Fast queue that avoids using potentially inefficient array .shift() calls // Based on https://github.com/creationix/fastqueue /** @private */ class Queue { constructor() { this._head = []; this._headLength = 0; this._tail = []; this._index = 0; this._length = 0; } get length() { return this._length; } shift() { if (this._index >= this._headLength) { const t = this._head; t.length = 0; this._head = this._tail; this._tail = t; this._index = 0; this._headLength = this._head.length; if (!this._headLength) { return; } } const value = this._head[this._index]; if (this._index < 0) { delete this._head[this._index++]; } else { this._head[this._index++] = undefined; } this._length--; return value; } push(item) { this._length++; this._tail.push(item); return this; }; unshift(item) { this._head[--this._index] = item; this._length++; return this; } } /** * xeokit runtime statistics. * @type {{components: {models: number, objects: number, scenes: number, meshes: number}, memory: {indices: number, uvs: number, textures: number, materials: number, transforms: number, positions: number, programs: number, normals: number, meshes: number, colors: number}, build: {version: string}, client: {browser: string}, frame: {frameCount: number, useProgram: number, bindTexture: number, drawElements: number, bindArray: number, tasksRun: number, fps: number, drawArrays: number, tasksScheduled: number}}} */ const stats = { build: { version: "0.8" }, client: { browser: (navigator && navigator.userAgent) ? navigator.userAgent : "n/a" }, components: { scenes: 0, models: 0, meshes: 0, objects: 0 }, memory: { meshes: 0, positions: 0, colors: 0, normals: 0, uvs: 0, indices: 0, textures: 0, transforms: 0, materials: 0, programs: 0 }, frame: { frameCount: 0, fps: 0, useProgram: 0, bindTexture: 0, bindArray: 0, drawElements: 0, drawArrays: 0, tasksRun: 0, tasksScheduled: 0 } }; /** * @private */ function xmlToJson(node, attributeRenamer) { if (node.nodeType === node.TEXT_NODE) { var v = node.nodeValue; if (v.match(/^\s+$/) === null) { return v; } } else if (node.nodeType === node.ELEMENT_NODE || node.nodeType === node.DOCUMENT_NODE) { var json = {type: node.nodeName, children: []}; if (node.nodeType === node.ELEMENT_NODE) { for (var j = 0; j < node.attributes.length; j++) { var attribute = node.attributes[j]; var nm = attributeRenamer[attribute.nodeName] || attribute.nodeName; json[nm] = attribute.nodeValue; } } for (var i = 0; i < node.childNodes.length; i++) { var item = node.childNodes[i]; var j = xmlToJson(item, attributeRenamer); if (j) json.children.push(j); } return json; } } /** * @private */ function clone(ob) { return JSON.parse(JSON.stringify(ob)); } /** * @private */ var guidChars = [["0", 10], ["A", 26], ["a", 26], ["_", 1], ["$", 1]].map(function (a) { var li = []; var st = a[0].charCodeAt(0); var en = st + a[1]; for (var i = st; i < en; ++i) { li.push(i); } return String.fromCharCode.apply(null, li); }).join(""); /** * @private */ function b64(v, len) { var r = (!len || len === 4) ? [0, 6, 12, 18] : [0, 6]; return r.map(function (i) { return guidChars.substr(parseInt(v / (1 << i)) % 64, 1) }).reverse().join(""); } /** * @private */ function compressGuid(g) { var bs = [0, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24, 26, 28, 30].map(function (i) { return parseInt(g.substr(i, 2), 16); }); return b64(bs[0], 2) + [1, 4, 7, 10, 13].map(function (i) { return b64((bs[i] << 16) + (bs[i + 1] << 8) + bs[i + 2]); }).join(""); } /** * @private */ function findNodeOfType(m, t) { var li = []; var _ = function (n) { if (n.type === t) li.push(n); (n.children || []).forEach(function (c) { _(c); }); }; _(m); return li; } /** * @private */ function timeout(dt) { return new Promise(function (resolve, reject) { setTimeout(resolve, dt); }); } /** * @private */ function httpRequest(args) { return new Promise(function (resolve, reject) { var xhr = new XMLHttpRequest(); xhr.open(args.method || "GET", args.url, true); xhr.onload = function (e) { if (xhr.readyState === 4) { if (xhr.status === 200) { resolve(xhr.responseXML); } else { reject(xhr.statusText); } } }; xhr.send(null); }); } /** * @private */ const queryString = function () { // This function is anonymous, is executed immediately and // the return value is assigned to QueryString! var query_string = {}; var query = window.location.search.substring(1); var vars = query.split("&"); for (var i = 0; i < vars.length; i++) { var pair = vars[i].split("="); // If first entry with this name if (typeof query_string[pair[0]] === "undefined") { query_string[pair[0]] = decodeURIComponent(pair[1]); // If second entry with this name } else if (typeof query_string[pair[0]] === "string") { var arr = [query_string[pair[0]], decodeURIComponent(pair[1])]; query_string[pair[0]] = arr; // If third or later entry with this name } else { query_string[pair[0]].push(decodeURIComponent(pair[1])); } } return query_string; }(); /** * @private */ function loadJSON(url, ok, err) { // Avoid checking ok and err on each use. var defaultCallback = (_value) => undefined; ok = ok || defaultCallback; err = err || defaultCallback; var request = new XMLHttpRequest(); request.overrideMimeType("application/json"); request.open('GET', url, true); request.addEventListener('load', function (event) { var response = event.target.response; if (this.status === 200) { var json; try { json = JSON.parse(response); } catch (e) { err(`utils.loadJSON(): Failed to parse JSON response - ${e}`); } ok(json); } else if (this.status === 0) { // Some browsers return HTTP Status 0 when using non-http protocol // e.g. 'file://' or 'data://'. Handle as success. console.warn('loadFile: HTTP Status 0 received.'); try { ok(JSON.parse(response)); } catch (e) { err(`utils.loadJSON(): Failed to parse JSON response - ${e}`); } } else { err(event); } }, false); request.addEventListener('error', function (event) { err(event); }, false); request.send(null); } /** * @private */ function loadArraybuffer$1(url, ok, err) { // Check for data: URI var defaultCallback = (_value) => undefined; ok = ok || defaultCallback; err = err || defaultCallback; const dataUriRegex = /^data:(.*?)(;base64)?,(.*)$/; const dataUriRegexResult = url.match(dataUriRegex); if (dataUriRegexResult) { // Safari can't handle data URIs through XMLHttpRequest const isBase64 = !!dataUriRegexResult[2]; var data = dataUriRegexResult[3]; data = window.decodeURIComponent(data); if (isBase64) { data = window.atob(data); } try { const buffer = new ArrayBuffer(data.length); const view = new Uint8Array(buffer); for (var i = 0; i < data.length; i++) { view[i] = data.charCodeAt(i); } core.scheduleTask(() => { ok(buffer); }); } catch (error) { core.scheduleTask(() => { err(error); }); } } else { const request = new XMLHttpRequest(); request.open('GET', url, true); request.responseType = 'arraybuffer'; request.onreadystatechange = function () { if (request.readyState === 4) { if (request.status === 200) { ok(request.response); } else { err('loadArrayBuffer error : ' + request.response); } } }; request.send(null); } } /** Tests if the given object is an array @private */ function isArray$1(value) { return value && !(value.propertyIsEnumerable('length')) && typeof value === 'object' && typeof value.length === 'number'; } /** Tests if the given value is a string @param value @returns {Boolean} @private */ function isString(value) { return (typeof value === 'string' || value instanceof String); } /** Tests if the given value is a number @param value @returns {Boolean} @private */ function isNumeric(value) { return !isNaN(parseFloat(value)) && isFinite(value); } /** Tests if the given value is an ID @param value @returns {Boolean} @private */ function isID(value) { return utils.isString(value) || utils.isNumeric(value); } /** Tests if the given components are the same, where the components can be either IDs or instances. @param c1 @param c2 @returns {Boolean} @private */ function isSameComponent(c1, c2) { if (!c1 || !c2) { return false; } const id1 = (utils.isNumeric(c1) || utils.isString(c1)) ? `${c1}` : c1.id; const id2 = (utils.isNumeric(c2) || utils.isString(c2)) ? `${c2}` : c2.id; return id1 === id2; } /** Tests if the given value is a function @param value @returns {Boolean} @private */ function isFunction$1(value) { return (typeof value === "function"); } /** Tests if the given value is a JavaScript JSON object, eg, ````{ foo: "bar" }````. @param value @returns {Boolean} @private */ function isObject$1(value) { const objectConstructor = {}.constructor; return (!!value && value.constructor === objectConstructor); } /** Returns a shallow copy */ function copy(o) { return utils.apply(o, {}); } /** Add properties of o to o2, overwriting them on o2 if already there */ function apply(o, o2) { for (const name in o) { if (o.hasOwnProperty(name)) { o2[name] = o[name]; } } return o2; } /** Add non-null/defined properties of o to o2 @private */ function apply2(o, o2) { for (const name in o) { if (o.hasOwnProperty(name)) { if (o[name] !== undefined && o[name] !== null) { o2[name] = o[name]; } } } return o2; } /** Add properties of o to o2 where undefined or null on o2 @private */ function applyIf(o, o2) { for (const name in o) { if (o.hasOwnProperty(name)) { if (o2[name] === undefined || o2[name] === null) { o2[name] = o[name]; } } } return o2; } /** Returns true if the given map is empty. @param obj @returns {Boolean} @private */ function isEmptyObject$1(obj) { for (const name in obj) { if (obj.hasOwnProperty(name)) { return false; } } return true; } /** Returns the given ID as a string, in quotes if the ID was a string to begin with. This is useful for logging IDs. @param {Number| String} id The ID @returns {String} @private */ function inQuotes(id) { return utils.isNumeric(id) ? (`${id}`) : (`'${id}'`); } /** Returns the concatenation of two typed arrays. @param a @param b @returns {*|a} @private */ function concat(a, b) { const c = new a.constructor(a.length + b.length); c.set(a); c.set(b, a.length); return c; } function flattenParentChildHierarchy(root) { var list = []; function visit(node) { node.id = node.uuid; delete node.oid; list.push(node); var children = node.children; if (children) { for (var i = 0, len = children.length; i < len; i++) { const child = children[i]; child.parent = node.id; visit(children[i]); } } node.children = []; } visit(root); return list; } /** * @private */ const utils = { xmlToJson: xmlToJson, clone: clone, compressGuid: compressGuid, findNodeOfType: findNodeOfType, timeout: timeout, httpRequest: httpRequest, loadJSON: loadJSON, loadArraybuffer: loadArraybuffer$1, queryString: queryString, isArray: isArray$1, isString: isString, isNumeric: isNumeric, isID: isID, isSameComponent: isSameComponent, isFunction: isFunction$1, isObject: isObject$1, copy: copy, apply: apply, apply2: apply2, applyIf: applyIf, isEmptyObject: isEmptyObject$1, inQuotes: inQuotes, concat: concat, flattenParentChildHierarchy: flattenParentChildHierarchy }; const scenesRenderInfo = {}; // Used for throttling FPS for each Scene const sceneIDMap = new Map$1(); // Ensures unique scene IDs const taskQueue = new Queue(); // Task queue, which is pumped on each frame; tasks are pushed to it with calls to xeokit.schedule const tickEvent = {sceneId: null, time: null, startTime: null, prevTime: null, deltaTime: null}; const taskBudget = 10; // Millisecs we're allowed to spend on tasks in each frame const fpsSamples = []; const numFPSSamples = 30; let lastTime = 0; let elapsedTime; let totalFPS = 0; /** * @private */ function Core() { /** Semantic version number. The value for this is set by an expression that's concatenated to the end of the built binary by the xeokit build script. @property version @namespace xeokit @type {String} */ this.version = "1.0.0"; /** Existing {@link Scene}s , mapped to their IDs @property scenes @namespace xeokit @type {Scene} */ this.scenes = {}; this._superTypes = {}; // For each component type, a list of its supertypes, ordered upwards in the hierarchy. /** * Registers a scene on xeokit. * This is called within the xeokit.Scene constructor. * @private */ this._addScene = function (scene) { if (scene.id) { // User-supplied ID if (core.scenes[scene.id]) { console.error(`[ERROR] Scene ${utils.inQuotes(scene.id)} already exists`); return; } } else { // Auto-generated ID scene.id = sceneIDMap.addItem({}); } core.scenes[scene.id] = scene; const ticksPerOcclusionTest = scene.ticksPerOcclusionTest; const ticksPerRender = scene.ticksPerRender; scenesRenderInfo[scene.id] = { ticksPerOcclusionTest: ticksPerOcclusionTest, ticksPerRender: ticksPerRender, renderCountdown: ticksPerRender }; stats.components.scenes++; scene.once("destroyed", () => { // Unregister destroyed scenes sceneIDMap.removeItem(scene.id); delete core.scenes[scene.id]; delete scenesRenderInfo[scene.id]; stats.components.scenes--; }); }; /** * @private */ this.clear = function () { let scene; for (const id in core.scenes) { if (core.scenes.hasOwnProperty(id)) { scene = core.scenes[id]; // Only clear the default Scene // but destroy all the others if (id === "default.scene") { scene.clear(); } else { scene.destroy(); delete core.scenes[scene.id]; } } } }; /** * Schedule a task to run at the next frame. * * Internally, this pushes the task to a FIFO queue. Within each frame interval, xeokit processes the queue * for a certain period of time, popping tasks and running them. After each frame interval, tasks that did not * get a chance to run during the task are left in the queue to be run next time. * * @param {Function} callback Callback that runs the task. * @param {Object} [scope] Scope for the callback. */ this.scheduleTask = function (callback, scope = null) { taskQueue.push(callback); taskQueue.push(scope); }; this.runTasks = function (until = -1) { // Pops and processes tasks in the queue, until the given number of milliseconds has elapsed. let time = (new Date()).getTime(); let callback; let scope; let tasksRun = 0; while (taskQueue.length > 0 && (until < 0 || time < until)) { callback = taskQueue.shift(); scope = taskQueue.shift(); if (scope) { callback.call(scope); } else { callback(); } time = (new Date()).getTime(); tasksRun++; } return tasksRun; }; this.getNumTasks = function () { return taskQueue.length; }; } /** * @private * @type {Core} */ const core = new Core(); const frame = function () { let time = Date.now(); elapsedTime = time - lastTime; if (lastTime > 0 && elapsedTime > 0) { // Log FPS stats var newFPS = 1000 / elapsedTime; // Moving average of FPS totalFPS += newFPS; fpsSamples.push(newFPS); if (fpsSamples.length >= numFPSSamples) { totalFPS -= fpsSamples.shift(); } stats.frame.fps = Math.round(totalFPS / fpsSamples.length); } for (let id in core.scenes) { core.scenes[id].compile(); } runTasks(time); lastTime = time; }; function customSetInterval(callback, interval) { let expected = Date.now() + interval; function loop() { const elapsed = Date.now() - expected; callback(); expected += interval; setTimeout(loop, Math.max(0, interval - elapsed)); } loop(); return { cancel: function() { // No need to do anything, setTimeout cannot be directly cancelled } }; } customSetInterval(() => { frame(); }, 100); const renderFrame = function () { let time = Date.now(); elapsedTime = time - lastTime; if (lastTime > 0 && elapsedTime > 0) { // Log FPS stats var newFPS = 1000 / elapsedTime; // Moving average of FPS totalFPS += newFPS; fpsSamples.push(newFPS); if (fpsSamples.length >= numFPSSamples) { totalFPS -= fpsSamples.shift(); } stats.frame.fps = Math.round(totalFPS / fpsSamples.length); } runTasks(time); fireTickEvents(time); renderScenes(); (window.requestPostAnimationFrame !== undefined) ? window.requestPostAnimationFrame(frame) : requestAnimationFrame(renderFrame); }; renderFrame(); function runTasks(time) { // Process as many enqueued tasks as we can within the per-frame task budget const tasksRun = core.runTasks(time + taskBudget); const tasksScheduled = core.getNumTasks(); stats.frame.tasksRun = tasksRun; stats.frame.tasksScheduled = tasksScheduled; stats.frame.tasksBudget = taskBudget; } function fireTickEvents(time) { // Fire tick event on each Scene tickEvent.time = time; for (var id in core.scenes) { if (core.scenes.hasOwnProperty(id)) { var scene = core.scenes[id]; tickEvent.sceneId = id; tickEvent.startTime = scene.startTime; tickEvent.deltaTime = tickEvent.prevTime != null ? tickEvent.time - tickEvent.prevTime : 0; /** * Fired on each game loop iteration. * * @event tick * @param {String} sceneID The ID of this Scene. * @param {Number} startTime The time in seconds since 1970 that this Scene was instantiated. * @param {Number} time The time in seconds since 1970 of this "tick" event. * @param {Number} prevTime The time of the previous "tick" event from this Scene. * @param {Number} deltaTime The time in seconds since the previous "tick" event from this Scene. */ scene.fire("tick", tickEvent, true); } } tickEvent.prevTime = time; } function renderScenes() { const scenes = core.scenes; const forceRender = false; let scene; let renderInfo; let ticksPerOcclusionTest; let ticksPerRender; let id; for (id in scenes) { if (scenes.hasOwnProperty(id)) { scene = scenes[id]; renderInfo = scenesRenderInfo[id]; if (!renderInfo) { renderInfo = scenesRenderInfo[id] = {}; // FIXME } ticksPerOcclusionTest = scene.ticksPerOcclusionTest; if (renderInfo.ticksPerOcclusionTest !== ticksPerOcclusionTest) { renderInfo.ticksPerOcclusionTest = ticksPerOcclusionTest; renderInfo.renderCountdown = ticksPerOcclusionTest; } if (--scene.occlusionTestCountdown <= 0) { scene.doOcclusionTest(); scene.occlusionTestCountdown = ticksPerOcclusionTest; } ticksPerRender = scene.ticksPerRender; if (renderInfo.ticksPerRender !== ticksPerRender) { renderInfo.ticksPerRender = ticksPerRender; renderInfo.renderCountdown = ticksPerRender; } if (--renderInfo.renderCountdown === 0) { scene.render(forceRender); renderInfo.renderCountdown = ticksPerRender; } } } } /** * @desc Base class for all xeokit components. * * ## Component IDs * * Every Component has an ID that's unique within the parent {@link Scene}. xeokit generates * the IDs automatically by default, however you can also specify them yourself. In the example below, we're creating a * scene comprised of {@link Scene}, {@link Material}, {@link ReadableGeometry} and * {@link Mesh} components, while letting xeokit generate its own ID for * the {@link ReadableGeometry}: * *````JavaScript * import {Viewer, Mesh, buildTorusGeometry, ReadableGeometry, PhongMaterial, Texture, Fresnel} from "xeokit-sdk.es.js"; * * const viewer = new Viewer({ * canvasId: "myCanvas" * }); * * viewer.scene.camera.eye = [0, 0, 5]; * viewer.scene.camera.look = [0, 0, 0]; * viewer.scene.camera.up = [0, 1, 0]; * * new Mesh(viewer.scene, { * geometry: new ReadableGeometry(viewer.scene, buildTorusGeometry({ * center: [0, 0, 0], * radius: 1.5, * tube: 0.5, * radialSegments: 32, * tubeSegments: 24, * arc: Math.PI * 2.0 * }), * material: new PhongMaterial(viewer.scene, { * id: "myMaterial", * ambient: [0.9, 0.3, 0.9], * shininess: 30, * diffuseMap: new Texture(viewer.scene, { * src: "textures/diffuse/uvGrid2.jpg" * }), * specularFresnel: new Fresnel(viewer.scene, { * leftColor: [1.0, 1.0, 1.0], * rightColor: [0.0, 0.0, 0.0], * power: 4 * }) * }) * }); *```` * * We can then find those components like this: * * ````javascript * // Find the Material * var material = viewer.scene.components["myMaterial"]; * * // Find all PhongMaterials in the Scene * var phongMaterials = viewer.scene.types["PhongMaterial"]; * * // Find our Material within the PhongMaterials * var materialAgain = phongMaterials["myMaterial"]; * ```` * * ## Restriction on IDs * * Auto-generated IDs are of the form ````"__0"````, ````"__1"````, ````"__2"```` ... and so on. * * Scene maintains a map of these IDs, along with a counter that it increments each time it generates a new ID. * * If Scene has created the IDs listed above, and we then destroy the ````Component```` with ID ````"__1"````, * Scene will mark that ID as available, and will reuse it for the next default ID. * * Therefore, two restrictions your on IDs: * * * don't use IDs that begin with two underscores, and * * don't reuse auto-generated IDs of destroyed Components. * * ## Logging * * Components have methods to log ID-prefixed messages to the JavaScript console: * * ````javascript * material.log("Everything is fine, situation normal."); * material.warn("Wait, whats that red light?"); * material.error("Aw, snap!"); * ```` * * The logged messages will look like this in the console: * * ````text * [LOG] myMaterial: Everything is fine, situation normal. * [WARN] myMaterial: Wait, whats that red light.. * [ERROR] myMaterial: Aw, snap! * ```` * * ## Destruction * * Get notification of destruction of Components: * * ````javascript * material.once("destroyed", function() { * this.log("Component was destroyed: " + this.id); * }); * ```` * * Or get notification of destruction of any Component within its {@link Scene}: * * ````javascript * scene.on("componentDestroyed", function(component) { * this.log("Component was destroyed: " + component.id); * }); * ```` * * Then destroy a component like this: * * ````javascript * material.destroy(); * ```` */ class Component { /** @private */ get type() { return "Component"; } /** * @private */ get isComponent() { return true; } constructor(owner = null, cfg = {}) { /** * The parent {@link Scene} that contains this Component. * * @property scene * @type {Scene} * @final */ this.scene = null; if (this.type === "Scene") { this.scene = this; /** * The viewer that contains this Scene. * @property viewer * @type {Viewer} */ this.viewer = cfg.viewer; } else { if (owner.type === "Scene") { this.scene = owner; } else if (owner instanceof Component) { this.scene = owner.scene; } else { throw "Invalid param: owner must be a Component" } this._owner = owner; } this._dontClear = !!cfg.dontClear; // Prevent Scene#clear from destroying this component this._renderer = this.scene._renderer; /** Arbitrary, user-defined metadata on this component. @property metadata @type Object */ this.meta = cfg.meta || {}; /** * ID of this Component, unique within the {@link Scene}. * * Components are mapped by this ID in {@link Scene#components}. * * @property id * @type {String|Number} */ this.id = cfg.id; // Auto-generated by Scene by default /** True as soon as this Component has been destroyed @property destroyed @type {Boolean} */ this.destroyed = false; this._attached = {}; // Attached components with names. this._attachments = null; // Attached components keyed to IDs - lazy-instantiated this._subIdMap = null; // Subscription subId pool this._subIdEvents = null; // Subscription subIds mapped to event names this._eventSubs = null; // Event names mapped to subscribers this._eventSubsNum = null; this._events = null; // Maps names to events this._eventCallDepth = 0; // Helps us catch stack overflows from recursive events this._ownedComponents = null; // // Components created with #create - lazy-instantiated if (this !== this.scene) { // Don't add scene to itself this.scene._addComponent(this); // Assigns this component an automatic ID if not yet assigned } this._updateScheduled = false; // True when #_update will be called on next tick if (owner) { owner._own(this); } } // /** // * Unique ID for this Component within its {@link Scene}. // * // * @property // * @type {String} // */ // get id() { // return this._id; // } /** Indicates that we need to redraw the scene. This is called by certain subclasses after they have made some sort of state update that requires the renderer to perform a redraw. For example: a {@link Mesh} calls this on itself whenever you update its {@link Mesh#layer} property, which manually controls its render order in relation to other Meshes. If this component has a ````castsShadow```` property that's set ````true````, then this will also indicate that the renderer needs to redraw shadow map associated with this component. Components like {@link DirLight} have that property set when they produce light that creates shadows, while components like {@link Mesh"}}layer{{/crossLink}} have that property set when they cast shadows. @protected */ glRedraw() { if (!this._renderer) { // Called from a constructor return; } this._renderer.imageDirty(); if (this.castsShadow) { // Light source or object this._renderer.shadowsDirty(); } } /** Indicates that we need to re-sort the renderer's state-ordered drawables list. For efficiency, the renderer keeps its list of drawables ordered so that runs of the same state updates can be combined. This method is called by certain subclasses after they have made some sort of state update that would require re-ordering of the drawables list. For example: a {@link DirLight} calls this on itself whenever you update {@link DirLight#dir}. @protected */ glResort() { if (!this._renderer) { // Called from a constructor return; } this._renderer.needStateSort(); } /** * The {@link Component} that owns the lifecycle of this Component, if any. * * When that component is destroyed, this component will be automatically destroyed also. * * Will be null if this Component has no owner. * * @property owner * @type {Component} */ get owner() { return this._owner; } /** * Tests if this component is of the given type, or is a subclass of the given type. * @type {Boolean} */ isType(type) { return this.type === type; } /** * Fires an event on this component. * * Notifies existing subscribers to the event, optionally retains the event to give to * any subsequent notifications on the event as they are made. * * @param {String} event The event type name * @param {Object} value The event parameters * @param {Boolean} [forget=false] When true, does not retain for subsequent subscribers */ fire(event, value, forget) { if (!this._events) { this._events = {}; } if (!this._eventSubs) { this._eventSubs = {}; this._eventSubsNum = {}; } if (forget !== true) { this._events[event] = value || true; // Save notification } const subs = this._eventSubs[event]; let sub; if (subs) { // Notify subscriptions for (const subId in subs) { if (subs.hasOwnProperty(subId)) { sub = subs[subId]; this._eventCallDepth++; if (this._eventCallDepth < 300) { sub.callback.call(sub.scope, value); } else { this.error("fire: potential stack overflow from recursive event '" + event + "' - dropping this event"); } this._eventCallDepth--; } } } } /** * Subscribes to an event on this component. * * The callback is be called with this component as scope. * * @param {String} event The event * @param {Function} callback Called fired on the event * @param {Object} [scope=this] Scope for the callback * @return {String} Handle to the subscription, which may be used to unsubscribe with {@link #off}. */ on(event, callback, scope) { if (!this._events) { this._events = {}; } if (!this._subIdMap) { this._subIdMap = new Map$1(); // Subscription subId pool } if (!this._subIdEvents) { this._subIdEvents = {}; } if (!this._eventSubs) { this._eventSubs = {}; } if (!this._eventSubsNum) { this._eventSubsNum = {}; } let subs = this._eventSubs[event]; if (!subs) { subs = {}; this._eventSubs[event] = subs; this._eventSubsNum[event] = 1; } else { this._eventSubsNum[event]++; } const subId = this._subIdMap.addItem(); // Create unique subId subs[subId] = { callback: callback, scope: scope || this }; this._subIdEvents[subId] = event; const value = this._events[event]; if (value !== undefined) { // A publication exists, notify callback immediately callback.call(scope || this, value); } return subId; } /** * Cancels an event subscription that was previously made with {@link Component#on} or {@link Component#once}. * * @param {String} subId Subscription ID */ off(subId) { if (subId === undefined || subId === null) { return; } if (!this._subIdEvents) { return; } const event = this._subIdEvents[subId]; if (event) { delete this._subIdEvents[subId]; const subs = this._eventSubs[event]; if (subs) { delete subs[subId]; this._eventSubsNum[event]--; } this._subIdMap.removeItem(subId); // Release subId } } /** * Subscribes to the next occurrence of the given event, then un-subscribes as soon as the event is subIdd. * * This is equivalent to calling {@link Component#on}, and then calling {@link Component#off} inside the callback function. * * @param {String} event Data event to listen to * @param {Function} callback Called when fresh data is available at the event * @param {Object} [scope=this] Scope for the callback */ once(event, callback, scope) { const self = this; const subId = this.on(event, function (value) { self.off(subId); callback.call(scope || this, value); }, scope); } /** * Returns true if there are any subscribers to the given event on this component. * * @param {String} event The event * @return {Boolean} True if there are any subscribers to the given event on this component. */ hasSubs(event) { return (this._eventSubsNum && (this._eventSubsNum[event] > 0)); } /** * Logs a console debugging message for this component. * * The console message will have this format: *````[LOG] [ : ````* * * Also fires the message as a "log" event on the parent {@link Scene}. * * @param {String} message The message to log */ log(message) { message = "[LOG]" + this._message(message); window.console.log(message); this.scene.fire("log", message); } _message(message) { return " [" + this.type + " " + utils.inQuotes(this.id) + "]: " + message; } /** * Logs a warning for this component to the JavaScript console. * * The console message will have this format: *````[WARN] [ =: ````* * * Also fires the message as a "warn" event on the parent {@link Scene}. * * @param {String} message The message to log */ warn(message) { message = "[WARN]" + this._message(message); window.console.warn(message); this.scene.fire("warn", message); } /** * Logs an error for this component to the JavaScript console. * * The console message will have this format: *````[ERROR] [ =: ````* * * Also fires the message as an "error" event on the parent {@link Scene}. * * @param {String} message The message to log */ error(message) { message = "[ERROR]" + this._message(message); window.console.error(message); this.scene.fire("error", message); } /** * Adds a child component to this. * * When component not given, attaches the scene's default instance for the given name (if any). * Publishes the new child component on this component, keyed to the given name. * * @param {*} params * @param {String} params.name component name * @param {Component} [params.component] The component * @param {String} [params.type] Optional expected type of base type of the child; when supplied, will * cause an exception if the given child is not the same type or a subtype of this. * @param {Boolean} [params.sceneDefault=false] * @param {Boolean} [params.sceneSingleton=false] * @param {Function} [params.onAttached] Optional callback called when component attached * @param {Function} [params.onAttached.callback] Callback function * @param {Function} [params.onAttached.scope] Optional scope for callback * @param {Function} [params.onDetached] Optional callback called when component is detached * @param {Function} [params.onDetached.callback] Callback function * @param {Function} [params.onDetached.scope] Optional scope for callback * @param {{String:Function}} [params.on] Callbacks to subscribe to properties on component * @param {Boolean} [params.recompiles=true] When true, fires "dirty" events on this component * @private */ _attach(params) { const name = params.name; if (!name) { this.error("Component 'name' expected"); return; } let component = params.component; const sceneDefault = params.sceneDefault; const sceneSingleton = params.sceneSingleton; const type = params.type; const on = params.on; const recompiles = params.recompiles !== false; // True when child given as config object, where parent manages its instantiation and destruction let managingLifecycle = false; if (component) { if (utils.isNumeric(component) || utils.isString(component)) { // Component ID given // Both numeric and string IDs are supported const id = component; component = this.scene.components[id]; if (!component) { // Quote string IDs in errors this.error("Component not found: " + utils.inQuotes(id)); return; } } } if (!component) { if (sceneSingleton === true) { // Using the first instance of the component type we find const instances = this.scene.types[type]; for (const id2 in instances) { if (instances.hasOwnProperty) { component = instances[id2]; break; } } if (!component) { this.error("Scene has no default component for '" + name + "'"); return null; } } else if (sceneDefault === true) { // Using a default scene component component = this.scene[name]; if (!component) { this.error("Scene has no default component for '" + name + "'"); return null; } } } if (component) { if (component.scene.id !== this.scene.id) { this.error("Not in same scene: " + component.type + " " + utils.inQuotes(component.id)); return; } if (type) { if (!component.isType(type)) { this.error("Expected a " + type + " type or subtype: " + component.type + " " + utils.inQuotes(component.id)); return; } } } if (!this._attachments) { this._attachments = {}; } const oldComponent = this._attached[name]; let subs; let i; let len; if (oldComponent) { if (component && oldComponent.id === component.id) { // Reject attempt to reattach same component return; } const oldAttachment = this._attachments[oldComponent.id]; // Unsubscribe from events on old component subs = oldAttachment.subs; for (i = 0, len = subs.length; i < len; i++) { oldComponent.off(subs[i]); } delete this._attached[name]; delete this._attachments[oldComponent.id]; const onDetached = oldAttachment.params.onDetached; if (onDetached) { if (utils.isFunction(onDetached)) { onDetached(oldComponent); } else { onDetached.scope ? onDetached.callback.call(onDetached.scope, oldComponent) : onDetached.callback(oldComponent); } } if (oldAttachment.managingLifecycle) { // Note that we just unsubscribed from all events fired by the child // component, so destroying it won't fire events back at us now. oldComponent.destroy(); } } if (component) { // Set and publish the new component on this component const attachment = { params: params, component: component, subs: [], managingLifecycle: managingLifecycle }; attachment.subs.push( component.once("destroyed", function () { attachment.params.component = null; this._attach(attachment.params); }, this)); if (recompiles) { attachment.subs.push( component.on("dirty", function () { this.fire("dirty", this); }, this)); } this._attached[name] = component; this._attachments[component.id] = attachment; // Bind destruct listener to new component to remove it // from this component when destroyed const onAttached = params.onAttached; if (onAttached) { if (utils.isFunction(onAttached)) { onAttached(component); } else { onAttached.scope ? onAttached.callback.call(onAttached.scope, component) : onAttached.callback(component); } } if (on) { let event; let subIdr; let callback; let scope; for (event in on) { if (on.hasOwnProperty(event)) { subIdr = on[event]; if (utils.isFunction(subIdr)) { callback = subIdr; scope = null; } else { callback = subIdr.callback; scope = subIdr.scope; } if (!callback) { continue; } attachment.subs.push(component.on(event, callback, scope)); } } } } if (recompiles) { this.fire("dirty", this); // FIXME: May trigger spurous mesh recompilations unless able to limit with param? } this.fire(name, component); // Component can be null return component; } _checkComponent(expectedType, component) { if (!component.isComponent) { if (utils.isID(component)) { const id = component; component = this.scene.components[id]; if (!component) { this.error("Component not found: " + id); return; } } else { this.error("Expected a Component or ID"); return; } } if (expectedType !== component.type) { this.error("Expected a " + expectedType + " Component"); return; } if (component.scene.id !== this.scene.id) { this.error("Not in same scene: " + component.type); return; } return component; } _checkComponent2(expectedTypes, component) { if (!component.isComponent) { if (utils.isID(component)) { const id = component; component = this.scene.components[id]; if (!component) { this.error("Component not found: " + id); return; } } else { this.error("Expected a Component or ID"); return; } } if (component.scene.id !== this.scene.id) { this.error("Not in same scene: " + component.type); return; } for (var i = 0, len = expectedTypes.length; i < len; i++) { if (expectedTypes[i] === component.type) { return component; } } this.error("Expected component types: " + expectedTypes); return null; } _own(component) { if (!this._ownedComponents) { this._ownedComponents = {}; } if (!this._ownedComponents[component.id]) { this._ownedComponents[component.id] = component; } component.once("destroyed", () => { delete this._ownedComponents[component.id]; }, this); } /** * Protected method, called by sub-classes to queue a call to _update(). * @protected * @param {Number} [priority=1] */ _needUpdate(priority) { if (!this._updateScheduled) { this._updateScheduled = true; if (priority === 0) { this._doUpdate(); } else { core.scheduleTask(this._doUpdate, this); } } } /** * @private */ _doUpdate() { if (this._updateScheduled) { this._updateScheduled = false; if (this._update) { this._update(); } } } /** * Schedule a task to perform on the next browser interval * @param task */ scheduleTask(task) { core.scheduleTask(task, null); } /** * Protected virtual template method, optionally implemented * by sub-classes to perform a scheduled task. * * @protected */ _update() { } /** * Destroys all {@link Component}s that are owned by this. These are Components that were instantiated with * this Component as their first constructor argument. */ clear() { if (this._ownedComponents) { for (var id in this._ownedComponents) { if (this._ownedComponents.hasOwnProperty(id)) { const component = this._ownedComponents[id]; component.destroy(); delete this._ownedComponents[id]; } } } } /** * Destroys this component. */ destroy() { if (this.destroyed) { return; } /** * Fired when this Component is destroyed. * @event destroyed */ this.fire("destroyed", this.destroyed = true); // Must fire before we blow away subscription maps, below // Unsubscribe from child components and destroy then let id; let attachment; let component; let subs; let i; let len; if (this._attachments) { for (id in this._attachments) { if (this._attachments.hasOwnProperty(id)) { attachment = this._attachments[id]; component = attachment.component; subs = attachment.subs; for (i = 0, len = subs.length; i < len; i++) { component.off(subs[i]); } if (attachment.managingLifecycle) { component.destroy(); } } } } if (this._ownedComponents) { for (id in this._ownedComponents) { if (this._ownedComponents.hasOwnProperty(id)) { component = this._ownedComponents[id]; component.destroy(); delete this._ownedComponents[id]; } } } this.scene._removeComponent(this); // Memory leak avoidance this._attached = {}; this._attachments = null; this._subIdMap = null; this._subIdEvents = null; this._eventSubs = null; this._events = null; this._eventCallDepth = 0; this._ownedComponents = null; this._updateScheduled = false; } } const tempVec3a$O = math.vec3(); const tempVec3b$C = math.vec3(); const tempMat4a$s = math.mat4(); /** * @private */ class FrustumPlane { constructor() { this.normal = math.vec3(); this.offset = 0; this.testVertex = math.vec3(); } set(nx, ny, nz, offset) { const s = 1.0 / Math.sqrt(nx * nx + ny * ny + nz * nz); this.normal[0] = nx * s; this.normal[1] = ny * s; this.normal[2] = nz * s; this.offset = offset * s; this.testVertex[0] = (this.normal[0] >= 0.0) ? 1 : 0; this.testVertex[1] = (this.normal[1] >= 0.0) ? 1 : 0; this.testVertex[2] = (this.normal[2] >= 0.0) ? 1 : 0; } } /** * @private */ class Frustum$1 { constructor() { this.planes = [ new FrustumPlane(), new FrustumPlane(), new FrustumPlane(), new FrustumPlane(), new FrustumPlane(), new FrustumPlane() ]; } } Frustum$1.INSIDE = 0; Frustum$1.INTERSECT = 1; Frustum$1.OUTSIDE = 2; /** @private */ function setFrustum(frustum, viewMat, projMat) { const m = math.mulMat4(projMat, viewMat, tempMat4a$s); const m0 = m[0]; const m1 = m[1]; const m2 = m[2]; const m3 = m[3]; const m4 = m[4]; const m5 = m[5]; const m6 = m[6]; const m7 = m[7]; const m8 = m[8]; const m9 = m[9]; const m10 = m[10]; const m11 = m[11]; const m12 = m[12]; const m13 = m[13]; const m14 = m[14]; const m15 = m[15]; frustum.planes[0].set(m3 - m0, m7 - m4, m11 - m8, m15 - m12); frustum.planes[1].set(m3 + m0, m7 + m4, m11 + m8, m15 + m12); frustum.planes[2].set(m3 - m1, m7 - m5, m11 - m9, m15 - m13); frustum.planes[3].set(m3 + m1, m7 + m5, m11 + m9, m15 + m13); frustum.planes[4].set(m3 - m2, m7 - m6, m11 - m10, m15 - m14); frustum.planes[5].set(m3 + m2, m7 + m6, m11 + m10, m15 + m14); } /** @private */ function frustumIntersectsAABB3(frustum, aabb) { let ret = Frustum$1.INSIDE; const min = tempVec3a$O; const max = tempVec3b$C; min[0] = aabb[0]; min[1] = aabb[1]; min[2] = aabb[2]; max[0] = aabb[3]; max[1] = aabb[4]; max[2] = aabb[5]; const bminmax = [min, max]; for (let i = 0; i < 6; ++i) { const plane = frustum.planes[i]; if (((plane.normal[0] * bminmax[plane.testVertex[0]][0]) + (plane.normal[1] * bminmax[plane.testVertex[1]][1]) + (plane.normal[2] * bminmax[plane.testVertex[2]][2]) + (plane.offset)) < 0.0) { return Frustum$1.OUTSIDE; } if (((plane.normal[0] * bminmax[1 - plane.testVertex[0]][0]) + (plane.normal[1] * bminmax[1 - plane.testVertex[1]][1]) + (plane.normal[2] * bminmax[1 - plane.testVertex[2]][2]) + (plane.offset)) < 0.0) { ret = Frustum$1.INTERSECT; } } return ret; } /** * Picks a {@link Viewer}'s {@link Entity}s with a canvas-space 2D marquee box. * * [](https://xeokit.github.io/xeokit-sdk/examples/picking/#marqueePick_select) * * * [[Example 1: Select Objects with Marquee](https://xeokit.github.io/xeokit-sdk/examples/picking/#marqueePick_select)] * * [[Example 2: View-Fit Objects with Marquee](https://xeokit.github.io/xeokit-sdk/examples/picking/#marqueePick_viewFit)] * * # Usage * * In the example below, we * * 1. Create a {@link Viewer}, arrange the {@link Camera} * 2. Use an {@link XKTLoaderPlugin} to load a BIM model, * 3. Create a {@link ObjectsKdTree3} to automatically index the `Viewer's` {@link Entity}s for fast spatial lookup, * 4. Create a `MarqueePicker` to pick {@link Entity}s in the {@link Viewer}, using the {@link ObjectsKdTree3} to accelerate picking * 5. Create a {@link MarqueePickerMouseControl} to perform the marquee-picking with the `MarqueePicker`, using mouse input to draw the marquee box on the `Viewer's` canvas. * * When the {@link MarqueePickerMouseControl} is active: * * * Long-click, drag and release on the canvas to define a marque box that picks {@link Entity}s. * * Drag left-to-right to pick {@link Entity}s that intersect the box. * * Drag right-to-left to pick {@link Entity}s that are fully inside the box. * * On release, the `MarqueePicker` will fire a "picked" event with IDs of the picked {@link Entity}s, if any. * * Handling that event, we mark the {@link Entity}s as selected. * * Hold down CTRL to multi-pick. * * ````javascript * import { * Viewer, * XKTLoaderPlugin, * ObjectsKdTree3, * MarqueePicker, * MarqueePickerMouseControl * } from "xeokit-sdk.es.js"; * * // 1 * * const viewer = new Viewer({ * canvasId: "myCanvas" * }); * * viewer.scene.camera.eye = [14.9, 14.3, 5.4]; * viewer.scene.camera.look = [6.5, 8.3, -4.1]; * viewer.scene.camera.up = [-0.28, 0.9, -0.3]; * * // 2 * * const xktLoader = new XKTLoaderPlugin(viewer); * * const sceneModel = xktLoader.load({ * id: "myModel", * src: "../../assets/models/xkt/v8/ifc/HolterTower.ifc.xkt" * }); * * // 3 * * const objectsKdTree3 = new ObjectsKdTree3({viewer}); * * // 4 * * const marqueePicker = new MarqueePicker({viewer, objectsKdTree3}); * * // 5 * * const marqueePickerMouseControl = new MarqueePickerMouseControl({marqueePicker}); * * marqueePicker.on("clear", () => { * viewer.scene.setObjectsSelected(viewer.scene.selectedObjectIds, false); * }); * * marqueePicker.on("picked", (objectIds) => { * viewer.scene.setObjectsSelected(objectIds, true); * }); * * marqueePickerMouseControl.setActive(true); * ```` * * # Design Notes * * * The {@link ObjectsKdTree3} can be shared with any other components that want to use it to spatially search for {@link Entity}s. * * The {@link MarqueePickerMouseControl} can be replaced with other types of controllers (i.e. touch), or used alongside them. * * The `MarqueePicker` has no input handlers of its own, and provides an API through which to programmatically control marquee picking. By firing the "picked" events, `MarqueePicker` implements the *Blackboard Pattern*. */ class MarqueePicker extends Component { /** * Creates a MarqueePicker. * * @param {*} cfg Configuration * @param {Viewer} cfg.viewer The Viewer to pick Entities from. * @param {ObjectsKdTree3} cfg.objectsKdTree3 A k-d tree that indexes the Entities in the Viewer for fast spatial lookup. */ constructor(cfg = {}) { if (!cfg.viewer) { throw "[MarqueePicker] Missing config: viewer"; } if (!cfg.objectsKdTree3) { throw "[MarqueePicker] Missing config: objectsKdTree3"; } super(cfg.viewer.scene, cfg); this.viewer = cfg.viewer; this._objectsKdTree3 = cfg.objectsKdTree3; this._canvasMarqueeCorner1 = math.vec2(); this._canvasMarqueeCorner2 = math.vec2(); this._canvasMarquee = math.AABB2(); this._marqueeFrustum = new Frustum$1(); this._marqueeFrustumProjMat = math.mat4(); this._pickMode = false; this._marqueeElement = document.createElement('div'); document.body.appendChild(this._marqueeElement); this._marqueeElement.style.position = "absolute"; this._marqueeElement.style["z-index"] = "40000005"; this._marqueeElement.style.width = 8 + "px"; this._marqueeElement.style.height = 8 + "px"; this._marqueeElement.style.visibility = "hidden"; this._marqueeElement.style.top = 0 + "px"; this._marqueeElement.style.left = 0 + "px"; this._marqueeElement.style["box-shadow"] = "0 2px 5px 0 #182A3D;"; this._marqueeElement.style["opacity"] = 1.0; this._marqueeElement.style["pointer-events"] = "none"; } /** * Sets the canvas-space position of the first marquee box corner. * * @param corner1 */ setMarqueeCorner1(corner1) { this._canvasMarqueeCorner1.set(corner1); this._canvasMarqueeCorner2.set(corner1); this._updateMarquee(); } /** * Sets the canvas-space position of the second marquee box corner. * * @param corner2 */ setMarqueeCorner2(corner2) { this._canvasMarqueeCorner2.set(corner2); this._updateMarquee(); } /** * Sets both canvas-space corner positions of the marquee box. * * @param corner1 * @param corner2 */ setMarquee(corner1, corner2) { this._canvasMarqueeCorner1.set(corner1); this._canvasMarqueeCorner2.set(corner2); this._updateMarquee(); } /** * Sets if the marquee box is visible. * * @param {boolean} visible True if the marquee box is to be visible, else false. */ setMarqueeVisible(visible) { this._marqueVisible = visible; this._marqueeElement.style.visibility = visible ? "visible" : "hidden"; } /** * Gets if the marquee box is visible. * * @returns {boolean} True if the marquee box is visible, else false. */ getMarqueeVisible() { return this._marqueVisible; } /** * Sets the pick mode. * * Supported pick modes are: * * * MarqueePicker.PICK_MODE_INSIDE - picks {@link Entity}s that are completely inside the marquee box. * * MarqueePicker.PICK_MODE_INTERSECTS - picks {@link Entity}s that intersect the marquee box. * * @param {number} pickMode The pick mode. */ setPickMode(pickMode) { if (pickMode !== MarqueePicker.PICK_MODE_INSIDE && pickMode !== MarqueePicker.PICK_MODE_INTERSECTS) { throw "Illegal MarqueePicker pickMode: must be MarqueePicker.PICK_MODE_INSIDE or MarqueePicker.PICK_MODE_INTERSECTS"; } if (pickMode !== this._pickMode) { this._marqueeElement.style["background-image"] = pickMode === MarqueePicker.PICK_MODE_INSIDE /* Solid */ ? "url(\"data:image/svg+xml,%3csvg width='100%25' height='100%25' xmlns='http://www.w3.org/2000/svg'%3e%3crect width='100%25' height='100%25' fill='none' rx='6' ry='6' stroke='%23333' stroke-width='4'/%3e%3c/svg%3e\")" /* Dashed */ : "url(\"data:image/svg+xml,%3csvg width='100%25' height='100%25' xmlns='http://www.w3.org/2000/svg'%3e%3crect width='100%25' height='100%25' fill='none' rx='6' ry='6' stroke='%23333' stroke-width='4' stroke-dasharray='6%2c 14' stroke-dashoffset='0' stroke-linecap='square'/%3e%3c/svg%3e\")"; this._pickMode = pickMode; } } /** * Gets the pick mode. * * Supported pick modes are: * * * MarqueePicker.PICK_MODE_INSIDE - picks {@link Entity}s that are completely inside the marquee box. * * MarqueePicker.PICK_MODE_INTERSECTS - picks {@link Entity}s that intersect the marquee box. * * @returns {number} The pick mode. */ getPickMode() { return this._pickMode; } /** * Fires a "clear" event on this MarqueePicker. */ clear() { this.fire("clear", {}); } /** * Attempts to pick {@link Entity}s, using the current MarquePicker settings. * * Fires a "picked" event with the IDs of the {@link Entity}s that were picked, if any. * * @returns {string[]} IDs of the {@link Entity}s that were picked, if any */ pick() { this._updateMarquee(); this._buildMarqueeFrustum(); const entityIds = []; const visitNode = (node, intersects = Frustum$1.INTERSECT) => { if (intersects === Frustum$1.INTERSECT) { intersects = frustumIntersectsAABB3(this._marqueeFrustum, node.aabb); } if (intersects === Frustum$1.OUTSIDE) { return; } if (node.entities) { const entities = node.entities; for (let i = 0, len = entities.length; i < len; i++) { const entity = entities[i]; if (!entity.visible) { continue; } const entityAABB = entity.aabb; if (this._pickMode === MarqueePicker.PICK_MODE_INSIDE) { // Select entities that are completely inside marquee const intersection = frustumIntersectsAABB3(this._marqueeFrustum, entityAABB); if (intersection === Frustum$1.INSIDE) { entityIds.push(entity.id); } } else { // Select entities that are partially inside marquee const intersection = frustumIntersectsAABB3(this._marqueeFrustum, entityAABB); if (intersection !== Frustum$1.OUTSIDE) { entityIds.push(entity.id); } } } } if (node.left) { visitNode(node.left, intersects); } if (node.right) { visitNode(node.right, intersects); } }; if (this._canvasMarquee[2] - this._canvasMarquee[0] > 3 || this._canvasMarquee[3] - this._canvasMarquee[1] > 3) { // Marquee pick if rectangle big enough visitNode(this._objectsKdTree3.root); } this.fire("picked", entityIds); return entityIds; } _updateMarquee() { this._canvasMarquee[0] = Math.min(this._canvasMarqueeCorner1[0], this._canvasMarqueeCorner2[0]); this._canvasMarquee[1] = Math.min(this._canvasMarqueeCorner1[1], this._canvasMarqueeCorner2[1]); this._canvasMarquee[2] = Math.max(this._canvasMarqueeCorner1[0], this._canvasMarqueeCorner2[0]); this._canvasMarquee[3] = Math.max(this._canvasMarqueeCorner1[1], this._canvasMarqueeCorner2[1]); this._marqueeElement.style.width = `${this._canvasMarquee[2] - this._canvasMarquee[0]}px`; this._marqueeElement.style.height = `${this._canvasMarquee[3] - this._canvasMarquee[1]}px`; this._marqueeElement.style.left = `${this._canvasMarquee[0]}px`; this._marqueeElement.style.top = `${this._canvasMarquee[1]}px`; } _buildMarqueeFrustum() { // https://github.com/xeokit/xeokit-sdk/issues/869#issuecomment-1165375770 const canvas = this.viewer.scene.canvas.canvas; const canvasWidth = canvas.clientWidth; const canvasHeight = canvas.clientHeight; const canvasLeft = canvas.clientLeft; const canvasTop = canvas.clientTop; const xCanvasToClip = 2.0 / canvasWidth; const yCanvasToClip = 2.0 / canvasHeight; const NEAR_SCALING = 17; const ratio = canvas.clientHeight / canvas.clientWidth; const FAR_PLANE = 10000; const left = (this._canvasMarquee[0] - canvasLeft) * xCanvasToClip + -1; const right = (this._canvasMarquee[2] - canvasLeft) * xCanvasToClip + -1; const bottom = -(this._canvasMarquee[3] - canvasTop) * yCanvasToClip + 1; const top = -(this._canvasMarquee[1] - canvasTop) * yCanvasToClip + 1; const near = this.viewer.scene.camera.frustum.near * (NEAR_SCALING * ratio); const far = FAR_PLANE; math.frustumMat4( left, right, bottom * ratio, top * ratio, near, far, this._marqueeFrustumProjMat, ); setFrustum(this._marqueeFrustum, this.viewer.scene.camera.viewMatrix, this._marqueeFrustumProjMat); } /** * Destroys this MarqueePicker. * * Does not destroy the {@link Viewer} or the {@link ObjectsKdTree3} provided to the constructor of this MarqueePicker. */ destroy() { super.destroy(); if (this._marqueeElement.parentElement) { this._marqueeElement.parentElement.removeChild(this._marqueeElement); this._marqueeElement = null; this._objectsKdTree3 = null; } } } /** * Pick mode that picks {@link Entity}s that intersect the marquee box. * * @type {number} */ MarqueePicker.PICK_MODE_INTERSECTS = 0; /** * Pick mode that picks {@link Entity}s that are completely inside the marquee box. * * @type {number} */ MarqueePicker.PICK_MODE_INSIDE = 1; /** * Controls a {@link MarqueePicker} with mouse input. * * See {@link MarqueePicker} for usage example. * * When the MarqueePickerMouseControl is active: * * * Long-click, drag and release on the canvas to define a marque box that picks {@link Entity}s. * * Drag left-to-right to pick Entities that intersect the box. * * Drag right-to-left to pick Entities that are fully inside the box. * * On release, the MarqueePicker will fire a "picked" event with IDs of the picked Entities , if any. */ class MarqueePickerMouseControl extends Component { /** * Creates a new MarqueePickerMouseControl. * * @param {*} cfg Configuration * @param {MarqueePicker} cfg.marqueePicker The MarqueePicker to control. */ constructor(cfg) { super(cfg.marqueePicker, cfg); const marqueePicker = cfg.marqueePicker; const scene = marqueePicker.viewer.scene; const canvas = scene.canvas.canvas; let pageStartX; let pageStartY; let pageEndX; let pageEndY; let canvasStartX; let canvasEndX; let isMouseDragging = false; let isMouseDown = false; let mouseWasUpOffCanvas = false; let mouseDownTimer; canvas.addEventListener("mousedown", (e) => { if (!this.getActive()) { return; } if (e.button !== 0) { // Left button only return; } mouseDownTimer = setTimeout(function () { const input = marqueePicker.viewer.scene.input; if (!input.keyDown[input.KEY_CTRL]) { // Clear selection unless CTRL down marqueePicker.clear(); } pageStartX = e.pageX; pageStartY = e.pageY; canvasStartX = e.offsetX; marqueePicker.setMarqueeCorner1([pageStartX, pageStartY]); isMouseDragging = true; marqueePicker.viewer.cameraControl.pointerEnabled = false; // Disable camera rotation marqueePicker.setMarqueeVisible(true); canvas.style.cursor = "crosshair"; }, 400); isMouseDown = true; }); canvas.addEventListener("mouseup", (e) => { if (!this.getActive()) { return; } if (!isMouseDragging && !mouseWasUpOffCanvas) { return } if (e.button !== 0) { return; } clearTimeout(mouseDownTimer); pageEndX = e.pageX; pageEndY = e.pageY; const width = Math.abs(pageEndX - pageStartX); const height = Math.abs(pageEndY - pageStartY); isMouseDragging = false; marqueePicker.viewer.cameraControl.pointerEnabled = true; // Enable camera rotation if (mouseWasUpOffCanvas) { mouseWasUpOffCanvas = false; } if (width > 3 || height > 3) { // Marquee pick if rectangle big enough marqueePicker.pick(); } }); // Bubbling document.addEventListener("mouseup", (e) => { if (!this.getActive()) { return; } if (e.button !== 0) { // check if left button was clicked return; } clearTimeout(mouseDownTimer); if (!isMouseDragging) { return } marqueePicker.setMarqueeVisible(false); isMouseDragging = false; isMouseDown = false; mouseWasUpOffCanvas = true; marqueePicker.viewer.cameraControl.pointerEnabled = true; }, true); // Capturing canvas.addEventListener("mousemove", (e) => { if (!this.getActive()) { return; } if (e.button !== 0) { // check if left button was clicked return; } if (!isMouseDown) { return; } clearTimeout(mouseDownTimer); if (!isMouseDragging) { return } pageEndX = e.pageX; pageEndY = e.pageY; canvasEndX = e.offsetX; marqueePicker.setMarqueeVisible(true); marqueePicker.setMarqueeCorner2([pageEndX, pageEndY]); marqueePicker.setPickMode((canvasStartX < canvasEndX) ? MarqueePicker.PICK_MODE_INSIDE : MarqueePicker.PICK_MODE_INTERSECTS); }); } /** * Activates or deactivates this MarqueePickerMouseControl. * * @param {boolean} active Whether or not to activate. */ setActive(active) { if (this._active === active) { return; } this._active = active; this.fire("active", this._active); } /** * Gets if this MarqueePickerMouseControl is active. * * @returns {boolean} */ getActive() { return this._active; } /** * */ destroy() { } } /** * A PointerCircle shows a circle, centered at the position of the * mouse or touch pointer. */ class PointerCircle { /** * Constructs a new PointerCircle. * @param viewer The Viewer * @param [cfg] PointerCircle configuration. * @param [cfg.active=true] Whether PointerCircle is active. The PointerCircle can only be shown when this is `true` (default). */ constructor(viewer, cfg = {}) { this.viewer = viewer; this.scene = this.viewer.scene; this._circleDiv = document.createElement('div'); this.viewer.scene.canvas.canvas.parentNode.insertBefore(this._circleDiv, this.viewer.scene.canvas.canvas); this._circleDiv.style.backgroundColor = "transparent"; this._circleDiv.style.border = "2px solid green"; this._circleDiv.style.borderRadius = "50px"; this._circleDiv.style.width = "50px"; this._circleDiv.style.height = "50px"; this._circleDiv.style.margin = "-200px -200px"; this._circleDiv.style.zIndex = "100000"; this._circleDiv.style.position = "absolute"; this._circleDiv.style.pointerEvents = "none"; this._circlePos = null; this._circleMaxRadius = 200; this._circleMinRadius = 2; this._active = (cfg.active !== false); this._visible = false; this._running = false; this._destroyed = false; } /** * Show the circle at the given canvas coordinates and begin shrinking it. */ start(circlePos) { if (this._destroyed) { return; } this._circlePos = circlePos; this._running = false; this._circleRadius = this._circleMaxRadius; this._circleDiv.style.borderRadius = `${this._circleRadius}px`; this._circleDiv.style.marginLeft = `${this._circlePos[0] - this._circleRadius}px`; this._circleDiv.style.marginTop = `${this._circlePos[1] - this._circleRadius}px`; const startValue = this._circleMaxRadius; const endValue = 2; let startTime; const duration = 300; const animateCircle = (currentTime) => { if (!this._running) { return; } if (!startTime) { startTime = currentTime; } const elapsedTime = currentTime - startTime; const progress = Math.min(elapsedTime / duration, 1); const interpolatedValue = startValue + (endValue - startValue) * progress; this._circleRadius = interpolatedValue; this._circleDiv.style.width = `${this._circleRadius}px`; this._circleDiv.style.height = `${this._circleRadius}px`; this._circleDiv.style.marginLeft = `${this._circlePos[0] - this._circleRadius / 2}px`; this._circleDiv.style.marginTop = `${this._circlePos[1] - this._circleRadius / 2}px`; if (progress < 1) { requestAnimationFrame(animateCircle); } }; this._running = true; requestAnimationFrame(animateCircle); this._circleDiv.style.visibility = "visible"; } /** * Stop the shrinking circle and hide it. */ stop() { if (this._destroyed) { return; } this._running = false; this._circleRadius = this._circleMaxRadius; this._circleDiv.style.borderRadius = `${this._circleRadius}px`; this._circleDiv.style.visibility = "hidden"; } /** * Sets the zoom factor for the lens. * * This is `2` by default. * * @param durationMs */ set durationMs(durationMs) { this.stop(); this._durationMs = durationMs; } /** * Gets the zoom factor for the lens. * * This is `2` by default. * * @returns Number */ get durationMs() { return this._durationMs; } /** * Destroys this PointerCircle. */ destroy() { if (!this._destroyed) { this.stop(); this._circleDiv.parentElement.removeChild(this._circleDiv); this._destroyed = true; } } } /** * @desc Localization service for a {@link Viewer}. * * * A LocaleService is a container of string translations ("messages") for various locales. * * A {@link Viewer} has its own default LocaleService at {@link Viewer#localeService}. * * We can replace that with our own LocaleService, or a custom subclass, via the Viewer's constructor. * * Viewer plugins that need localized translations will attempt to them for the currently active locale from the LocaleService. * * Whenever we switch the LocaleService to a different locale, plugins will automatically refresh their translations for that locale. * * ## Usage * * In the example below, we'll create a {@link Viewer} that uses an {@link XKTLoaderPlugin} to load a BIM model, and a * {@link NavCubePlugin}, which shows a camera navigation cube in the corner of the canvas. * * We'll also configure our Viewer with our own LocaleService instance, configured with English, Māori and French * translations for our NavCubePlugin. * * We could instead have just used the Viewer's default LocaleService, but this example demonstrates how we might * configure the Viewer our own custom LocaleService subclass. * * The translations fetched by our NavCubePlugin will be: * * * "NavCube.front" * * "NavCube.back" * * "NavCube.top" * * "NavCube.bottom" * * "NavCube.left" * * "NavCube.right" * *
* These are paths that resolve to our translations for the currently active locale, and are hard-coded within * the NavCubePlugin. * * For example, if the LocaleService's locale is set to "fr", then the path "NavCube.back" will drill down * into ````messages->fr->NavCube->front```` and fetch "Arrière". * * If we didn't provide that particular translation in our LocaleService, or any translations for that locale, * then the NavCubePlugin will just fall back on its own default hard-coded translation, which in this case is "BACK". * * [[Run example](https://xeokit.github.io/xeokit-sdk/examples/index.html#localization_NavCubePlugin)] * * ````javascript * import {Viewer, LocaleService, NavCubePlugin, XKTLoaderPlugin} from "xeokit-sdk.es.js"; * * const viewer = new Viewer({ * * canvasId: "myCanvas", * * localeService: new LocaleService({ * messages: { * "en": { // English * "NavCube": { * "front": "Front", * "back": "Back", * "top": "Top", * "bottom": "Bottom", * "left": "Left", * "right": "Right" * } * }, * "mi": { // Māori * "NavCube": { * "front": "Mua", * "back": "Tuarā", * "top": "Runga", * "bottom": "Raro", * "left": "Mauī", * "right": "Tika" * } * }, * "fr": { // Francais * "NavCube": { * "front": "Avant", * "back": "Arrière", * "top": "Supérieur", * "bottom": "Inférieur", * "left": "Gauche", * "right": "Droit" * } * } * }, * locale: "en" * }) * }); * * viewer.camera.eye = [-3.93, 2.85, 27.01]; * viewer.camera.look = [4.40, 3.72, 8.89]; * viewer.camera.up = [-0.01, 0.99, 0.03]; * * const navCubePlugin = new NavCubePlugin(viewer, { * canvasID: "myNavCubeCanvas" * }); * * const xktLoader = new XKTLoaderPlugin(viewer); * * const model = xktLoader.load({ * id: "myModel", * src: "./models/xkt/Duplex.ifc.xkt", * edges: true * }); * ```` * * We can dynamically switch our Viewer to a different locale at any time, which will update the text on the * faces of our NavCube: * * ````javascript * viewer.localeService.locale = "mi"; // Switch to Māori * ```` * * We can load new translations at any time: * * ````javascript * viewer.localeService.loadMessages({ * "jp": { // Japanese * "NavCube": { * "front": "前部", * "back": "裏", * "top": "上", * "bottom": "底", * "left": "左", * "right": "右" * } * } * }); * ```` * * And we can clear the translations if needed: * * ````javascript * viewer.localeService.clearMessages(); * ```` * * We can get an "updated" event from the LocaleService whenever we switch locales or load messages, which is useful * for triggering UI elements to refresh themselves with updated translations. Internally, our {@link NavCubePlugin} * subscribes to this event, fetching new strings for itself via {@link LocaleService#translate} each time the * event is fired. * * ````javascript * viewer.localeService.on("updated", () => { * console.log( viewer.localeService.translate("NavCube.left") ); * }); * ```` * @since 2.0 */ class LocaleService { /** * Constructs a LocaleService. * * @param {*} [params={}] * @param {JSON} [params.messages] * @param {String} [params.locale] */ constructor(params = {}) { this._eventSubIDMap = null; this._eventSubEvents = null; this._eventSubs = null; this._events = null; this._locale = "en"; this._messages = {}; this._locales = []; this._locale = "en"; this.messages = params.messages; this.locale = params.locale; } /** * Replaces the current set of locale translations. * * * Fires an "updated" event when done. * * Automatically refreshes any plugins that depend on the translations. * * Does not change the current locale. * * ## Usage * * ````javascript * viewer.localeService.setMessages({ * messages: { * "en": { // English * "NavCube": { * "front": "Front", * "back": "Back", * "top": "Top", * "bottom": "Bottom", * "left": "Left", * "right": "Right" * } * }, * "mi": { // Māori * "NavCube": { * "front": "Mua", * "back": "Tuarā", * "top": "Runga", * "bottom": "Raro", * "left": "Mauī", * "right": "Tika" * } * } * } * }); * ```` * * @param {*} messages The new translations. */ set messages(messages) { this._messages = messages || {}; this._locales = Object.keys(this._messages); this.fire("updated", this); } /** * Loads a new set of locale translations, adding them to the existing translations. * * * Fires an "updated" event when done. * * Automatically refreshes any plugins that depend on the translations. * * Does not change the current locale. * * ## Usage * * ````javascript * viewer.localeService.loadMessages({ * "jp": { // Japanese * "NavCube": { * "front": "前部", * "back": "裏", * "top": "上", * "bottom": "底", * "left": "左", * "right": "右" * } * } * }); * ```` * * @param {*} messages The new translations. */ loadMessages(messages = {}) { for (let locale in messages) { this._messages[locale] = messages[locale]; } this.messages = this._messages; } /** * Clears all locale translations. * * * Fires an "updated" event when done. * * Does not change the current locale. * * Automatically refreshes any plugins that depend on the translations, which will cause those * plugins to fall back on their internal hard-coded text values, since this method removes all * our translations. */ clearMessages() { this.messages = {}; } /** * Gets the list of available locales. * * These are derived from the currently configured set of translations. * * @returns {String[]} The list of available locales. */ get locales() { return this._locales; } /** * Sets the current locale. * * * Fires an "updated" event when done. * * The given locale does not need to be in the list of available locales returned by {@link LocaleService#locales}, since * this method assumes that you may want to load the locales at a later point. * * Automatically refreshes any plugins that depend on the translations. * * We can then get translations for the locale, if translations have been loaded for it, via {@link LocaleService#translate} and {@link LocaleService#translatePlurals}. * * @param {String} locale The new current locale. */ set locale(locale) { locale = locale || "de"; if (this._locale === locale) { return; } this._locale = locale; this.fire("updated", locale); } /** * Gets the current locale. * * @returns {String} The current locale. */ get locale() { return this._locale; } /** * Translates the given string according to the current locale. * * Returns null if no translation can be found. * * @param {String} msg String to translate. * @param {*} [args] Extra parameters. * @returns {String|null} Translated string if found, else null. */ translate(msg, args) { const localeMessages = this._messages[this._locale]; if (!localeMessages) { return null; } const localeMessage = resolvePath$1(msg, localeMessages); if (localeMessage) { if (args) { return vsprintf(localeMessage, args); } return localeMessage; } return null; } /** * Translates the given phrase according to the current locale. * * Returns null if no translation can be found. * * @param {String} msg Phrase to translate. * @param {Number} count The plural number. * @param {*} [args] Extra parameters. * @returns {String|null} Translated string if found, else null. */ translatePlurals(msg, count, args) { const localeMessages = this._messages[this._locale]; if (!localeMessages) { return null; } let localeMessage = resolvePath$1(msg, localeMessages); count = parseInt("" + count, 10); if (count === 0) { localeMessage = localeMessage.zero; } else { localeMessage = (count > 1) ? localeMessage.other : localeMessage.one; } if (!localeMessage) { return null; } localeMessage = vsprintf(localeMessage, [count]); if (args) { localeMessage = vsprintf(localeMessage, args); } return localeMessage; } /** * Fires an event on this LocaleService. * * Notifies existing subscribers to the event, optionally retains the event to give to * any subsequent notifications on the event as they are made. * * @param {String} event The event type name. * @param {Object} value The event parameters. * @param {Boolean} [forget=false] When true, does not retain for subsequent subscribers. */ fire(event, value, forget) { if (!this._events) { this._events = {}; } if (!this._eventSubs) { this._eventSubs = {}; } if (forget !== true) { this._events[event] = value || true; // Save notification } const subs = this._eventSubs[event]; if (subs) { for (const subId in subs) { if (subs.hasOwnProperty(subId)) { const sub = subs[subId]; sub.callback(value); } } } } /** * Subscribes to an event on this LocaleService. * * @param {String} event The event * @param {Function} callback Callback fired on the event * @return {String} Handle to the subscription, which may be used to unsubscribe with {@link #off}. */ on(event, callback) { if (!this._events) { this._events = {}; } if (!this._eventSubIDMap) { this._eventSubIDMap = new Map$1(); // Subscription subId pool } if (!this._eventSubEvents) { this._eventSubEvents = {}; } if (!this._eventSubs) { this._eventSubs = {}; } let subs = this._eventSubs[event]; if (!subs) { subs = {}; this._eventSubs[event] = subs; } const subId = this._eventSubIDMap.addItem(); // Create unique subId subs[subId] = { callback: callback }; this._eventSubEvents[subId] = event; const value = this._events[event]; if (value !== undefined) { callback(value); } return subId; } /** * Cancels an event subscription that was previously made with {@link LocaleService#on}. * * @param {String} subId Subscription ID */ off(subId) { if (subId === undefined || subId === null) { return; } if (!this._eventSubEvents) { return; } const event = this._eventSubEvents[subId]; if (event) { delete this._eventSubEvents[subId]; const subs = this._eventSubs[event]; if (subs) { delete subs[subId]; } this._eventSubIDMap.removeItem(subId); // Release subId } } } function resolvePath$1(key, json) { if (json[key]) { return json[key]; } const parts = key.split("."); let obj = json; for (let i = 0, len = parts.length; obj && (i < len); i++) { const part = parts[i]; obj = obj[part]; } return obj; } function vsprintf(msg, args = []) { return msg.replace(/\{\{|\}\}|\{(\d+)\}/g, function (m, n) { if (m === "{{") { return "{"; } if (m === "}}") { return "}"; } return args[n]; }); } /** * @desc Abstract base class for curve classes. */ class Curve extends Component { /** * @constructor * @param {Component} [owner] Owner component. When destroyed, the owner will destroy this Curve as well. * @param {*} [cfg] Configs * @param {String} [cfg.id] Optional ID, unique among all components in the parent {@link Curve}, generated automatically when omitted. * @param {Object} [cfg] Configs for this Curve. * @param {Number} [cfg.t=0] Current position on this Curve, in range between ````0..1````. */ constructor(owner, cfg = {}) { super(owner, cfg); this.t = cfg.t; } /** * Sets the progress along this Curve. * * Automatically clamps to range ````[0..1]````. * * Default value is ````0````. * * @param {Number} value The progress value. */ set t(value) { value = value || 0; this._t = value < 0.0 ? 0.0 : (value > 1.0 ? 1.0 : value); } /** * Gets the progress along this Curve. * * @returns {Number} The progress value. */ get t() { return this._t; } /** * Gets the tangent on this Curve at position {@link Curve#t}. * * @returns {Number[]} The tangent. */ get tangent() { return this.getTangent(this._t); } /** * Gets the length of this Curve. * * @returns {Number} The Curve length. */ get length() { var lengths = this._getLengths(); return lengths[lengths.length - 1]; } /** * Returns a normalized tangent vector on this Curve at the given position. * * @param {Number} t Position to get tangent at. * @returns {Number[]} Normalized tangent vector */ getTangent(t) { var delta = 0.0001; if (t === undefined) { t = this._t; } var t1 = t - delta; var t2 = t + delta; if (t1 < 0) { t1 = 0; } if (t2 > 1) { t2 = 1; } var pt1 = this.getPoint(t1); var pt2 = this.getPoint(t2); var vec = math.subVec3(pt2, pt1, []); return math.normalizeVec3(vec, []); } getPointAt(u) { var t = this.getUToTMapping(u); return this.getPoint(t); } /** * Samples points on this Curve, at the given number of equally-spaced divisions. * * @param {Number} divisions The number of divisions. * @returns {{Array of Array}} Array of sampled 3D points. */ getPoints(divisions) { if (!divisions) { divisions = 5; } var d, pts = []; for (d = 0; d <= divisions; d++) { pts.push(this.getPoint(d / divisions)); } return pts; } _getLengths(divisions) { if (!divisions) { divisions = (this.__arcLengthDivisions) ? (this.__arcLengthDivisions) : 200; } if (this.cacheArcLengths && (this.cacheArcLengths.length === divisions + 1) && !this.needsUpdate) { return this.cacheArcLengths; } this.needsUpdate = false; var cache = []; var current; var last = this.getPoint(0); var p; var sum = 0; cache.push(0); for (p = 1; p <= divisions; p++) { current = this.getPoint(p / divisions); sum += math.lenVec3(math.subVec3(current, last, [])); cache.push(sum); last = current; } this.cacheArcLengths = cache; return cache; // { sums: cache, sum:sum }, Sum is in the last element. } _updateArcLengths() { this.needsUpdate = true; this._getLengths(); } // Given u ( 0 .. 1 ), get a t to find p. This gives you points which are equi distance getUToTMapping(u, distance) { var arcLengths = this._getLengths(); var i = 0; var il = arcLengths.length; var t; var targetArcLength; // The targeted u distance value to get if (distance) { targetArcLength = distance; } else { targetArcLength = u * arcLengths[il - 1]; } //var time = Date.now(); var low = 0, high = il - 1, comparison; while (low <= high) { i = Math.floor(low + (high - low) / 2); // less likely to overflow, though probably not issue here, JS doesn't really have integers, all numbers are floats comparison = arcLengths[i] - targetArcLength; if (comparison < 0) { low = i + 1; } else if (comparison > 0) { high = i - 1; } else { high = i; break; // DONE } } i = high; if (arcLengths[i] === targetArcLength) { t = i / (il - 1); return t; } var lengthBefore = arcLengths[i]; var lengthAfter = arcLengths[i + 1]; var segmentLength = lengthAfter - lengthBefore; var segmentFraction = (targetArcLength - lengthBefore) / segmentLength; t = (i + segmentFraction) / (il - 1); return t; } } /** * @desc A {@link Curve} along which a 3D position can be animated. * * * As shown in the diagram below, a SplineCurve is defined by three or more control points. * * You can sample a {@link SplineCurve#point} and a {@link Curve#tangent} vector on a SplineCurve for any given value of {@link SplineCurve#t} in the range ````[0..1]````. * * When you set {@link SplineCurve#t} on a SplineCurve, its {@link SplineCurve#point} and {@link Curve#tangent} will update accordingly. * * To build a complex path, you can combine an unlimited combination of SplineCurves, {@link CubicBezierCurve} and {@link QuadraticBezierCurve} into a {@link Path}. *
*
* * * Spline Curve from Wikipedia* */ class SplineCurve extends Curve { /** * @constructor * @param {Component} [owner] Owner component. When destroyed, the owner will destroy this SplineCurve as well. * @param {*} [cfg] Configs * @param {String} [cfg.id] Optional ID, unique among all components in the parent {@link Scene}, generated automatically when omitted. * @param {Array} [cfg.points=[]] Control points on this SplineCurve. * @param {Number} [cfg.t=0] Current position on this SplineCurve, in range between 0..1. * @param {Number} [cfg.t=0] Current position on this CubicBezierCurve, in range between 0..1. */ constructor(owner, cfg = {}) { super(owner, cfg); this.points = cfg.points; this.t = cfg.t; } /** * Sets the control points on this SplineCurve. * * Default value is ````[]````. * * @param {Number[]} value New control points. */ set points(value) { this._points = value || []; } /** * Gets the control points on this SplineCurve. * * Default value is ````[]````. * * @returns {Number[]} The control points. */ get points() { return this._points; } /** * Sets the progress along this SplineCurve. * * Automatically clamps to range ````[0..1]````. * * Default value is ````0````. * * @param {Number} value The new progress. */ set t(value) { value = value || 0; this._t = value < 0.0 ? 0.0 : (value > 1.0 ? 1.0 : value); } /** * Gets the progress along this SplineCurve. * * Automatically clamps to range ````[0..1]````. * * Default value is ````0````. * * @returns {Number} The new progress. */ get t() { return this._t; } /** * Gets the point on this SplineCurve at position {@link SplineCurve#t}. * * @returns {Number[]} The point at {@link SplineCurve#t}. */ get point() { return this.getPoint(this._t); } /** * Returns point on this SplineCurve at the given position. * * @param {Number} t Position to get point at. * @returns {Number[]} Point at the given position. */ getPoint(t) { var points = this.points; if (points.length < 3) { this.error("Can't sample point from SplineCurve - not enough points on curve - returning [0,0,0]."); return; } var point = (points.length - 1) * t; var intPoint = Math.floor(point); var weight = point - intPoint; var point0 = points[intPoint === 0 ? intPoint : intPoint - 1]; var point1 = points[intPoint]; var point2 = points[intPoint > points.length - 2 ? points.length - 1 : intPoint + 1]; var point3 = points[intPoint > points.length - 3 ? points.length - 1 : intPoint + 2]; var vector = math.vec3(); vector[0] = math.catmullRomInterpolate(point0[0], point1[0], point2[0], point3[0], weight); vector[1] = math.catmullRomInterpolate(point0[1], point1[1], point2[1], point3[1], weight); vector[2] = math.catmullRomInterpolate(point0[2], point1[2], point2[2], point3[2], weight); return vector; } getJSON() { return { points: points, t: this._t }; } } const tempVec3a$N = math.vec3(); /** * @desc Defines a sequence of frames along which a {@link CameraPathAnimation} can animate a {@link Camera}. * * See {@link CameraPathAnimation} for usage. */ class CameraPath extends Component { /** * Returns "CameraPath". * * @private * * @returns {string} "CameraPath" */ get type() { return "CameraPath" } /** * @constructor * @param {Component} [owner] Owner component. When destroyed, the owner will destroy this CameraPath as well. * @param [cfg] {*} Configuration * @param {String} [cfg.id] Optional ID, unique among all components in the parent {@link Scene}, generated automatically when omitted. * @param {{t:Number, eye:Object, look:Object, up: Object}[]} [cfg.frames] Initial sequence of frames. */ constructor(owner, cfg = {}) { super(owner, cfg); this._frames = []; this._eyeCurve = new SplineCurve(this); this._lookCurve = new SplineCurve(this); this._upCurve = new SplineCurve(this); if (cfg.frames) { this.addFrames(cfg.frames); this.smoothFrameTimes(1); } } /** * Gets the camera frames in this CameraPath. * * @returns {{t:Number, eye:Object, look:Object, up: Object}[]} The frames on this CameraPath. */ get frames() { return this._frames; } /** * Gets the {@link SplineCurve} along which {@link Camera#eye} travels. * @returns {SplineCurve} The SplineCurve for {@link Camera#eye}. */ get eyeCurve() { return this._eyeCurve; } /** * Gets the {@link SplineCurve} along which {@link Camera#look} travels. * @returns {SplineCurve} The SplineCurve for {@link Camera#look}. */ get lookCurve() { return this._lookCurve; } /** * Gets the {@link SplineCurve} along which {@link Camera#up} travels. * @returns {SplineCurve} The SplineCurve for {@link Camera#up}. */ get upCurve() { return this._upCurve; } /** * Adds a frame to this CameraPath, given as the current position of the {@link Camera}. * * @param {Number} t Time instant for the new frame. */ saveFrame(t) { const camera = this.scene.camera; this.addFrame(t, camera.eye, camera.look, camera.up); } /** * Adds a frame to this CameraPath, specified as values for eye, look and up vectors at a given time instant. * * @param {Number} t Time instant for the new frame. * @param {Number[]} eye A three-element vector specifying the eye position for the new frame. * @param {Number[]} look A three-element vector specifying the look position for the new frame. * @param {Number[]} up A three-element vector specifying the up vector for the new frame. */ addFrame(t, eye, look, up) { const frame = { t: t, eye: eye.slice(0), look: look.slice(0), up: up.slice(0) }; this._frames.push(frame); this._eyeCurve.points.push(frame.eye); this._lookCurve.points.push(frame.look); this._upCurve.points.push(frame.up); } /** * Adds multiple frames to this CameraPath, each frame specified as a set of values for eye, look and up vectors at a given time instant. * * @param {{t:Number, eye:Object, look:Object, up: Object}[]} frames Frames to add to this CameraPath. */ addFrames(frames) { let frame; for (let i = 0, len = frames.length; i < len; i++) { frame = frames[i]; this.addFrame(frame.t || 0, frame.eye, frame.look, frame.up); } } /** * Sets the position of the {@link Camera} to a position interpolated within this CameraPath at the given time instant. * * @param {Number} t Time instant. */ loadFrame(t) { const camera = this.scene.camera; t = t / (this._frames[this._frames.length - 1].t - this._frames[0].t); t = t < 0.0 ? 0.0 : (t > 1.0 ? 1.0 : t); camera.eye = this._eyeCurve.getPoint(t, tempVec3a$N); camera.look = this._lookCurve.getPoint(t, tempVec3a$N); camera.up = this._upCurve.getPoint(t, tempVec3a$N); } /** * Gets eye, look and up vectors on this CameraPath at a given instant. * * @param {Number} t Time instant. * @param {Number[]} eye The eye position to update. * @param {Number[]} look The look position to update. * @param {Number[]} up The up vector to update. */ sampleFrame(t, eye, look, up) { t = t < 0.0 ? 0.0 : (t > 1.0 ? 1.0 : t); this._eyeCurve.getPoint(t, eye); this._lookCurve.getPoint(t, look); this._upCurve.getPoint(t, up); } /** * Given a total duration (in seconds) for this CameraPath, recomputes the time instant at each frame so that, * when animated by {@link CameraPathAnimation}, the {@link Camera} will move along the path at a constant rate. * * @param {Number} duration The total duration for this CameraPath. */ smoothFrameTimes(duration) { const numFrames = this._frames.length; if (numFrames === 0) { return; } const vec = math.vec3(); var totalLen = 0; this._frames[0].t = 0; const lens = []; for (let i = 1, len = this._frames.length; i < len; i++) { var lenVec = math.lenVec3(math.subVec3(this._frames[i].eye, this._frames[i - 1].eye, vec)); lens[i] = lenVec; totalLen += lenVec; } for (let i = 1, len = this._frames.length; i < len; i++) { const interFrameRate = (lens[i] / totalLen) * duration; this._frames[i].t = this._frames[i-1].t + interFrameRate; } } /** * Removes all frames from this CameraPath. */ clearFrames() { this._frames = []; this._eyeCurve.points = []; this._lookCurve.points = []; this._upCurve.points = []; } } const tempVec3$6 = math.vec3(); const newLook = math.vec3(); const newEye = math.vec3(); const newUp = math.vec3(); const newLookEyeVec = math.vec3(); /** * @desc Jumps or flies the {@link Scene}'s {@link Camera} to a given target. * * * Located at {@link Viewer#cameraFlight} * * Can fly or jump to its target. * * While flying, can be stopped, or redirected to a different target. * * Can also smoothly transition between ortho and perspective projections. * * * A CameraFlightAnimation's target can be: * * * specific ````eye````, ````look```` and ````up```` positions, * * an axis-aligned World-space bounding box (AABB), or * * an instance or ID of any {@link Component} subtype that provides a World-space AABB. * * A target can also contain a ````projection```` type to transition into. For example, if your {@link Camera#projection} is * currently ````"perspective"```` and you supply {@link CameraFlightAnimation#flyTo} with a ````projection```` property * equal to "ortho", then CameraFlightAnimation will smoothly transition the Camera into an orthographic projection. * * Configure {@link CameraFlightAnimation#fit} and {@link CameraFlightAnimation#fitFOV} to make it stop at the point * where the target occupies a certain amount of the field-of-view. * * ## Flying to an Entity * * Flying to an {@link Entity}: * * ````Javascript * var entity = new Mesh(viewer.scene); * * // Fly to the Entity's World-space AABB * viewer.cameraFlight.flyTo(entity); * ```` * ## Flying to a Position * * Flying the CameraFlightAnimation from the previous example to specified eye, look and up positions: * * ````Javascript * viewer.cameraFlight.flyTo({ * eye: [-5,-5,-5], * look: [0,0,0] * up: [0,1,0], * duration: 1 // Default, seconds * },() => { * // Done * }); * ```` * * ## Flying to an AABB * * Flying the CameraFlightAnimation from the previous two examples explicitly to the {@link Boundary3D"}}Boundary3D's{{/crossLink}} * axis-aligned bounding box: * * ````Javascript * viewer.cameraFlight.flyTo(entity.aabb); * ```` * * ## Transitioning Between Projections * * CameraFlightAnimation also allows us to smoothly transition between Camera projections. We can do that by itself, or * in addition to flying the Camera to a target. * * Let's transition the Camera to orthographic projection: * * [[Run example](/examples/index.html#camera_CameraFlightAnimation_projection)] * * ````Javascript * viewer.cameraFlight.flyTo({ projection: "ortho", () => { * // Done * }); * ```` * * Now let's transition the Camera back to perspective projection: * * ````Javascript * viewer.cameraFlight.flyTo({ projection: "perspective"}, () => { * // Done * }); * ```` * * Fly Camera to a position, while transitioning to orthographic projection: * * ````Javascript * viewer.cameraFlight.flyTo({ * eye: [-100,20,2], * look: [0,0,-40], * up: [0,1,0], * projection: "ortho", () => { * // Done * }); * ```` */ class CameraFlightAnimation extends Component { /** * @private */ get type() { return "CameraFlightAnimation"; } /** @constructor @private */ constructor(owner, cfg = {}) { super(owner, cfg); this._look1 = math.vec3(); this._eye1 = math.vec3(); this._up1 = math.vec3(); this._look2 = math.vec3(); this._eye2 = math.vec3(); this._up2 = math.vec3(); this._orthoScale1 = 1; this._orthoScale2 = 1; this._flying = false; this._flyEyeLookUp = false; this._flyingEye = false; this._flyingLook = false; this._callback = null; this._callbackScope = null; this._time1 = null; this._time2 = null; this.easing = cfg.easing !== false; this.duration = cfg.duration; this.fit = cfg.fit; this.fitFOV = cfg.fitFOV; this.trail = cfg.trail; } /** * Flies the {@link Camera} to a target. * * * When the target is a boundary, the {@link Camera} will fly towards the target and stop when the target fills most of the canvas. * * When the target is an explicit {@link Camera} position, given as ````eye````, ````look```` and ````up````, then CameraFlightAnimation will interpolate the {@link Camera} to that target and stop there. * * @param {Object|Component} [params=Scene] Either a parameters object or a {@link Component} subtype that has * an AABB. Defaults to the {@link Scene}, which causes the {@link Camera} to fit the Scene in view. * @param {Number} [params.arc=0] Factor in range ````[0..1]```` indicating how much the {@link Camera#eye} position * will swing away from its {@link Camera#look} position as it flies to the target. * @param {Number|String|Component} [params.component] ID or instance of a component to fly to. Defaults to the entire {@link Scene}. * @param {Number[]} [params.aabb] World-space axis-aligned bounding box (AABB) target to fly to. * @param {Number[]} [params.eye] Position to fly the eye position to. * @param {Number[]} [params.look] Position to fly the look position to. * @param {Number[]} [params.up] Position to fly the up vector to. * @param {String} [params.projection] Projection type to transition into as we fly. Can be any of the values of {@link Camera.projection}. * @param {Boolean} [params.fit=true] Whether to fit the target to the view volume. Overrides {@link CameraFlightAnimation#fit}. * @param {Number} [params.fitFOV] How much of field-of-view, in degrees, that a target {@link Entity} or its AABB should * fill the canvas on arrival. Overrides {@link CameraFlightAnimation#fitFOV}. * @param {Number} [params.duration] Flight duration in seconds. Overrides {@link CameraFlightAnimation#duration}. * @param {Number} [params.orthoScale] Animate the Camera's orthographic scale to this target value. See {@link Ortho#scale}. * @param {Function} [callback] Callback fired on arrival. * @param {Object} [scope] Optional scope for callback. */ flyTo(params, callback, scope) { params = params || this.scene; if (this._flying) { this.stop(); } this._flying = false; this._flyingEye = false; this._flyingLook = false; this._flyingEyeLookUp = false; this._callback = callback; this._callbackScope = scope; const camera = this.scene.camera; const flyToProjection = (!!params.projection) && (params.projection !== camera.projection); this._eye1[0] = camera.eye[0]; this._eye1[1] = camera.eye[1]; this._eye1[2] = camera.eye[2]; this._look1[0] = camera.look[0]; this._look1[1] = camera.look[1]; this._look1[2] = camera.look[2]; this._up1[0] = camera.up[0]; this._up1[1] = camera.up[1]; this._up1[2] = camera.up[2]; this._orthoScale1 = camera.ortho.scale; this._orthoScale2 = params.orthoScale || this._orthoScale1; let aabb; let eye; let look; let up; let componentId; if (params.aabb) { aabb = params.aabb; } else if (params.length === 6) { aabb = params; } else if ((params.eye && params.look) || params.up) { eye = params.eye; look = params.look; up = params.up; } else if (params.eye) { eye = params.eye; } else if (params.look) { look = params.look; } else { // Argument must be an instance or ID of a Component (subtype) let component = params; if (utils.isNumeric(component) || utils.isString(component)) { componentId = component; component = this.scene.components[componentId]; if (!component) { this.error("Component not found: " + utils.inQuotes(componentId)); if (callback) { if (scope) { callback.call(scope); } else { callback(); } } return; } } if (!flyToProjection) { aabb = component.aabb || this.scene.aabb; } } const poi = params.poi; if (aabb) { if (aabb[3] < aabb[0] || aabb[4] < aabb[1] || aabb[5] < aabb[2]) { // Don't fly to an inverted boundary return; } if (aabb[3] === aabb[0] && aabb[4] === aabb[1] && aabb[5] === aabb[2]) { // Don't fly to an empty boundary return; } aabb = aabb.slice(); const aabbCenter = math.getAABB3Center(aabb); this._look2 = poi || aabbCenter; const eyeLookVec = math.subVec3(this._eye1, this._look1, tempVec3$6); const eyeLookVecNorm = math.normalizeVec3(eyeLookVec); const diag = poi ? math.getAABB3DiagPoint(aabb, poi) : math.getAABB3Diag(aabb); const fitFOV = params.fitFOV || this._fitFOV; const sca = Math.abs(diag / Math.tan(fitFOV * math.DEGTORAD)); this._orthoScale2 = diag * 1.1; this._eye2[0] = this._look2[0] + (eyeLookVecNorm[0] * sca); this._eye2[1] = this._look2[1] + (eyeLookVecNorm[1] * sca); this._eye2[2] = this._look2[2] + (eyeLookVecNorm[2] * sca); this._up2[0] = this._up1[0]; this._up2[1] = this._up1[1]; this._up2[2] = this._up1[2]; this._flyingEyeLookUp = true; } else if (eye || look || up) { this._flyingEyeLookUp = !!eye && !!look && !!up; this._flyingEye = !!eye && !look; this._flyingLook = !!look && !eye; if (eye) { this._eye2[0] = eye[0]; this._eye2[1] = eye[1]; this._eye2[2] = eye[2]; } if (look) { this._look2[0] = look[0]; this._look2[1] = look[1]; this._look2[2] = look[2]; } if (up) { this._up2[0] = up[0]; this._up2[1] = up[1]; this._up2[2] = up[2]; } } if (flyToProjection) { if (params.projection === "ortho" && camera.projection !== "ortho") { this._projection2 = "ortho"; this._projMatrix1 = camera.projMatrix.slice(); this._projMatrix2 = camera.ortho.matrix.slice(); camera.projection = "customProjection"; } if (params.projection === "perspective" && camera.projection !== "perspective") { this._projection2 = "perspective"; this._projMatrix1 = camera.projMatrix.slice(); this._projMatrix2 = camera.perspective.matrix.slice(); camera.projection = "customProjection"; } } else { this._projection2 = null; } this.fire("started", params, true); this._time1 = Date.now(); this._time2 = this._time1 + (Number.isFinite(params.duration) ? params.duration * 1000 : this._duration); this._flying = true; // False as soon as we stop core.scheduleTask(this._update, this); } /** * Jumps the {@link Scene}'s {@link Camera} to the given target. * * * When the target is a boundary, this CameraFlightAnimation will position the {@link Camera} at where the target fills most of the canvas. * * When the target is an explicit {@link Camera} position, given as ````eye````, ````look```` and ````up```` vectors, then this CameraFlightAnimation will jump the {@link Camera} to that target. * * @param {*|Component} params Either a parameters object or a {@link Component} subtype that has a World-space AABB. * @param {Number} [params.arc=0] Factor in range [0..1] indicating how much the {@link Camera#eye} will swing away from its {@link Camera#look} as it flies to the target. * @param {Number|String|Component} [params.component] ID or instance of a component to fly to. * @param {Number[]} [params.aabb] World-space axis-aligned bounding box (AABB) target to fly to. * @param {Number[]} [params.eye] Position to fly the eye position to. * @param {Number[]} [params.look] Position to fly the look position to. * @param {Number[]} [params.up] Position to fly the up vector to. * @param {String} [params.projection] Projection type to transition into. Can be any of the values of {@link Camera.projection}. * @param {Number} [params.fitFOV] How much of field-of-view, in degrees, that a target {@link Entity} or its AABB should fill the canvas on arrival. Overrides {@link CameraFlightAnimation#fitFOV}. * @param {Boolean} [params.fit] Whether to fit the target to the view volume. Overrides {@link CameraFlightAnimation#fit}. */ jumpTo(params) { this._jumpTo(params); } _jumpTo(params) { if (this._flying) { this.stop(); } const camera = this.scene.camera; var aabb; var componentId; var newEye; var newLook; var newUp; if (params.aabb) { // Boundary3D aabb = params.aabb; } else if (params.length === 6) { // AABB aabb = params; } else if (params.eye || params.look || params.up) { // Camera pose newEye = params.eye; newLook = params.look; newUp = params.up; } else { // Argument must be an instance or ID of a Component (subtype) let component = params; if (utils.isNumeric(component) || utils.isString(component)) { componentId = component; component = this.scene.components[componentId]; if (!component) { this.error("Component not found: " + utils.inQuotes(componentId)); return; } } aabb = component.aabb || this.scene.aabb; } const poi = params.poi; if (aabb) { if (aabb[3] <= aabb[0] || aabb[4] <= aabb[1] || aabb[5] <= aabb[2]) { // Don't fly to an empty boundary return; } var diag = poi ? math.getAABB3DiagPoint(aabb, poi) : math.getAABB3Diag(aabb); newLook = poi || math.getAABB3Center(aabb, newLook); if (this._trail) { math.subVec3(camera.look, newLook, newLookEyeVec); } else { math.subVec3(camera.eye, camera.look, newLookEyeVec); } math.normalizeVec3(newLookEyeVec); let dist; const fit = (params.fit !== undefined) ? params.fit : this._fit; if (fit) { dist = Math.abs((diag) / Math.tan((params.fitFOV || this._fitFOV) * math.DEGTORAD)); } else { dist = math.lenVec3(math.subVec3(camera.eye, camera.look, tempVec3$6)); } math.mulVec3Scalar(newLookEyeVec, dist); camera.eye = math.addVec3(newLook, newLookEyeVec, tempVec3$6); camera.look = newLook; this.scene.camera.ortho.scale = diag * 1.1; } else if (newEye || newLook || newUp) { if (newEye) { camera.eye = newEye; } if (newLook) { camera.look = newLook; } if (newUp) { camera.up = newUp; } } if (params.projection) { camera.projection = params.projection; } } _update() { if (!this._flying) { return; } const time = Date.now(); let t = (time - this._time1) / (this._time2 - this._time1); const stopping = (t >= 1); if (t > 1) { t = 1; } const tFlight = this.easing ? CameraFlightAnimation._ease(t, 0, 1, 1) : t; const camera = this.scene.camera; if (this._flyingEye || this._flyingLook) { if (this._flyingEye) { math.subVec3(camera.eye, camera.look, newLookEyeVec); camera.eye = math.lerpVec3(tFlight, 0, 1, this._eye1, this._eye2, newEye); camera.look = math.subVec3(newEye, newLookEyeVec, newLook); } else if (this._flyingLook) { camera.look = math.lerpVec3(tFlight, 0, 1, this._look1, this._look2, newLook); camera.up = math.lerpVec3(tFlight, 0, 1, this._up1, this._up2, newUp); } } else if (this._flyingEyeLookUp) { camera.eye = math.lerpVec3(tFlight, 0, 1, this._eye1, this._eye2, newEye); camera.look = math.lerpVec3(tFlight, 0, 1, this._look1, this._look2, newLook); camera.up = math.lerpVec3(tFlight, 0, 1, this._up1, this._up2, newUp); } if (this._projection2) { const tProj = (this._projection2 === "ortho") ? CameraFlightAnimation._easeOutExpo(t, 0, 1, 1) : CameraFlightAnimation._easeInCubic(t, 0, 1, 1); camera.customProjection.matrix = math.lerpMat4(tProj, 0, 1, this._projMatrix1, this._projMatrix2); } else { camera.ortho.scale = this._orthoScale1 + (t * (this._orthoScale2 - this._orthoScale1)); } if (stopping) { camera.ortho.scale = this._orthoScale2; this.stop(); return; } core.scheduleTask(this._update, this); // Keep flying } static _ease(t, b, c, d) { // Quadratic easing out - decelerating to zero velocity http://gizma.com/easing t /= d; return -c * t * (t - 2) + b; } static _easeInCubic(t, b, c, d) { t /= d; return c * t * t * t + b; } static _easeOutExpo(t, b, c, d) { return c * (-Math.pow(2, -10 * t / d) + 1) + b; } /** * Stops an earlier flyTo, fires arrival callback. */ stop() { if (!this._flying) { return; } this._flying = false; this._time1 = null; this._time2 = null; if (this._projection2) { this.scene.camera.projection = this._projection2; } const callback = this._callback; if (callback) { this._callback = null; if (this._callbackScope) { callback.call(this._callbackScope); } else { callback(); } } this.fire("stopped", true, true); } /** * Cancels an earlier flyTo without calling the arrival callback. */ cancel() { if (!this._flying) { return; } this._flying = false; this._time1 = null; this._time2 = null; if (this._callback) { this._callback = null; } this.fire("canceled", true, true); } /** * Sets the flight duration, in seconds, when calling {@link CameraFlightAnimation#flyTo}. * * Stops any flight currently in progress. * * default value is ````0.5````. * * @param {Number} value New duration value. */ set duration(value) { this._duration = Number.isFinite(value) ? (value * 1000.0) : 500; this.stop(); } /** * Gets the flight duration, in seconds, when calling {@link CameraFlightAnimation#flyTo}. * * default value is ````0.5````. * * @returns {Number} New duration value. */ get duration() { return this._duration / 1000.0; } /** * Sets if, when CameraFlightAnimation is flying to a boundary, it will always adjust the distance between the * {@link Camera#eye} and {@link Camera#look} so as to ensure that the target boundary is always filling the view volume. * * When false, the eye will remain at its current distance from the look position. * * Default value is ````true````. * * @param {Boolean} value Set ````true```` to activate this behaviour. */ set fit(value) { this._fit = value !== false; } /** * Gets if, when CameraFlightAnimation is flying to a boundary, it will always adjust the distance between the * {@link Camera#eye} and {@link Camera#look} so as to ensure that the target boundary is always filling the view volume. * * When false, the eye will remain at its current distance from the look position. * * Default value is ````true````. * * @returns {Boolean} value Set ````true```` to activate this behaviour. */ get fit() { return this._fit; } /** * Sets how much of the perspective field-of-view, in degrees, that a target {@link Entity#aabb} should * fill the canvas when calling {@link CameraFlightAnimation#flyTo} or {@link CameraFlightAnimation#jumpTo}. * * Default value is ````45````. * * @param {Number} value New FOV value. */ set fitFOV(value) { this._fitFOV = value || 45; } /** * Gets how much of the perspective field-of-view, in degrees, that a target {@link Entity#aabb} should * fill the canvas when calling {@link CameraFlightAnimation#flyTo} or {@link CameraFlightAnimation#jumpTo}. * * Default value is ````45````. * * @returns {Number} Current FOV value. */ get fitFOV() { return this._fitFOV; } /** * Sets if this CameraFlightAnimation to point the {@link Camera} * in the direction that it is travelling when flying to a target after calling {@link CameraFlightAnimation#flyTo}. * * Default value is ````true````. * * @param {Boolean} value Set ````true```` to activate trailing behaviour. */ set trail(value) { this._trail = !!value; } /** * Gets if this CameraFlightAnimation points the {@link Camera} * in the direction that it is travelling when flying to a target after calling {@link CameraFlightAnimation#flyTo}. * * Default value is ````true````. * * @returns {Boolean} True if trailing behaviour is active. */ get trail() { return this._trail; } /** * @private */ destroy() { this.stop(); super.destroy(); } } /** * @desc Animates the {@link Scene}'s's {@link Camera} along a {@link CameraPath}. * * ## Usage * * In the example below, we'll load a model using a {@link GLTFLoaderPlugin}, then animate a {@link Camera} * through the frames in a {@link CameraPath}. * * * [[Run this example](/examples/index.html#camera_CameraPathAnimation)] * * ````Javascript * import {Viewer, GLTFLoaderPlugin, CameraPath, CameraPathAnimation} from "xeokit-sdk.es.js"; * * // Create a Viewer and arrange camera * * const viewer = new Viewer({ * canvasId: "myCanvas", * transparent: true * }); * * viewer.camera.eye = [124.86756896972656, -93.50288391113281, 173.2632598876953]; * viewer.camera.look = [102.14186096191406, -90.24193572998047, 173.4224395751953]; * viewer.camera.up = [0.23516440391540527, 0.9719591736793518, -0.0016466031083837152]; * * // Load model * * const gltfLoader = new GLTFLoaderPlugin(viewer); * * const model = gltfLoader.load({ * id: "myModel", * src: "./models/gltf/modern_office/scene.gltf", * edges: true, * edgeThreshold: 20, * xrayed: false * }); * * // Create a CameraPath * * var cameraPath = new CameraPath(viewer.scene, { * frames: [ * { * t: 0, * eye: [124.86, -93.50, 173.26], * look: [102.14, -90.24, 173.42], * up: [0.23, 0.97, -0.00] * }, * { * t: 1, * eye: [79.75, -85.98, 226.57], * look: [99.24, -84.11, 238.56], * up: [-0.14, 0.98, -0.09] * }, * // Rest of the frames omitted for brevity * ] * }); * * // Create a CameraPathAnimation to play our CameraPath * * var cameraPathAnimation = new CameraPathAnimation(viewer.scene, { * cameraPath: cameraPath, * playingRate: 0.2 // Playing 0.2 time units per second * }); * * // Once model loaded, start playing after a couple of seconds delay * * model.on("loaded", function () { * setTimeout(function () { * cameraPathAnimation.play(0); // Play from the beginning of the CameraPath * }, 2000); * }); * ```` */ class CameraPathAnimation extends Component { /** * Returns "CameraPathAnimation". * * @private * @returns {string} "CameraPathAnimation" */ get type() { return "CameraPathAnimation" } /** * @constructor * @param {Component} [owner] Owner component. When destroyed, the owner will destroy this CameraPathAnimation as well. * @param {*} [cfg] Configuration * @param {String} [cfg.id] Optional ID, unique among all components in the parent {@link Scene}, generated automatically when omitted. * @param {CameraPath} [cfg.eyeCurve] A {@link CameraPath} that defines the path of a {@link Camera}. */ constructor(owner, cfg = {}) { super(owner, cfg); this._cameraFlightAnimation = new CameraFlightAnimation(this); this._t = 0; this.state = CameraPathAnimation.SCRUBBING; this._playingFromT = 0; this._playingToT = 0; this._playingRate = cfg.playingRate || 1.0; this._playingDir = 1.0; this._lastTime = null; this.cameraPath = cfg.cameraPath; this._tick = this.scene.on("tick", this._updateT, this); } _updateT() { const cameraPath = this._cameraPath; if (!cameraPath) { return; } let numFrames; let t; const time = performance.now(); const elapsedSecs = (this._lastTime) ? (time - this._lastTime) * 0.001 : 0; this._lastTime = time; if (elapsedSecs === 0) { return; } switch (this.state) { case CameraPathAnimation.SCRUBBING: return; case CameraPathAnimation.PLAYING: this._t += this._playingRate * elapsedSecs; numFrames = this._cameraPath.frames.length; if (numFrames === 0 || (this._playingDir < 0 && this._t <= 0) || (this._playingDir > 0 && this._t >= this._cameraPath.frames[numFrames - 1].t)) { this.state = CameraPathAnimation.SCRUBBING; this._t = this._cameraPath.frames[numFrames - 1].t; this.fire("stopped"); return; } cameraPath.loadFrame(this._t); break; case CameraPathAnimation.PLAYING_TO: t = this._t + (this._playingRate * elapsedSecs * this._playingDir); if ((this._playingDir < 0 && t <= this._playingToT) || (this._playingDir > 0 && t >= this._playingToT)) { t = this._playingToT; this.state = CameraPathAnimation.SCRUBBING; this.fire("stopped"); } this._t = t; cameraPath.loadFrame(this._t); break; } } /* * @private */ _ease(t, b, c, d) { t /= d; return -c * t * (t - 2) + b; } /** * Sets the {@link CameraPath} animated by this CameraPathAnimation. * @param {CameraPath} value The new CameraPath. */ set cameraPath(value) { this._cameraPath = value; } /** * Gets the {@link CameraPath} animated by this CameraPathAnimation. * @returns {CameraPath} The CameraPath. */ get cameraPath() { return this._cameraPath; } /** * Sets the rate at which the CameraPathAnimation animates the {@link Camera} along the {@link CameraPath}. * * @param {Number} value The amount of progress per second. */ set rate(value) { this._playingRate = value; } /** * Gets the rate at which the CameraPathAnimation animates the {@link Camera} along the {@link CameraPath}. * * @returns {*|number} The current playing rate. */ get rate() { return this._playingRate; } /** * Begins animating the {@link Camera} along CameraPathAnimation's {@link CameraPath} from the beginning. */ play() { if (!this._cameraPath) { return; } this._lastTime = null; this.state = CameraPathAnimation.PLAYING; } /** * Begins animating the {@link Camera} along CameraPathAnimation's {@link CameraPath} from the given time. * * @param {Number} t Time instant. */ playToT(t) { const cameraPath = this._cameraPath; if (!cameraPath) { return; } this._playingFromT = this._t; this._playingToT = t; this._playingDir = (this._playingToT - this._playingFromT) < 0 ? -1 : 1; this._lastTime = null; this.state = CameraPathAnimation.PLAYING_TO; } /** * Animates the {@link Camera} along CameraPathAnimation's {@link CameraPath} to the given frame. * * @param {Number} frameIdx Index of the frame to play to. */ playToFrame(frameIdx) { const cameraPath = this._cameraPath; if (!cameraPath) { return; } const frame = cameraPath.frames[frameIdx]; if (!frame) { this.error("playToFrame - frame index out of range: " + frameIdx); return; } this.playToT(frame.t); } /** * Flies the {@link Camera} directly to the given frame on the CameraPathAnimation's {@link CameraPath}. * * @param {Number} frameIdx Index of the frame to play to. * @param {Function} [ok] Callback to fire when playing is complete. */ flyToFrame(frameIdx, ok) { const cameraPath = this._cameraPath; if (!cameraPath) { return; } const frame = cameraPath.frames[frameIdx]; if (!frame) { this.error("flyToFrame - frame index out of range: " + frameIdx); return; } this.state = CameraPathAnimation.SCRUBBING; this._cameraFlightAnimation.flyTo(frame, ok); } /** * Scrubs the {@link Camera} to the given time on the CameraPathAnimation's {@link CameraPath}. * * @param {Number} t Time instant. */ scrubToT(t) { const cameraPath = this._cameraPath; if (!cameraPath) { return; } const camera = this.scene.camera; if (!camera) { return; } this._t = t; cameraPath.loadFrame(this._t); this.state = CameraPathAnimation.SCRUBBING; } /** * Scrubs the {@link Camera} to the given frame on the CameraPathAnimation's {@link CameraPath}. * * @param {Number} frameIdx Index of the frame to scrub to. */ scrubToFrame(frameIdx) { const cameraPath = this._cameraPath; if (!cameraPath) { return; } const camera = this.scene.camera; if (!camera) { return; } const frame = cameraPath.frames[frameIdx]; if (!frame) { this.error("playToFrame - frame index out of range: " + frameIdx); return; } cameraPath.loadFrame(this._t); this.state = CameraPathAnimation.SCRUBBING; } /** * Stops playing this CameraPathAnimation. */ stop() { this.state = CameraPathAnimation.SCRUBBING; this.fire("stopped"); } destroy() { super.destroy(); this.scene.off(this._tick); } } CameraPathAnimation.STOPPED = 0; CameraPathAnimation.SCRUBBING = 1; CameraPathAnimation.PLAYING = 2; CameraPathAnimation.PLAYING_TO = 3; /** * @desc Defines a shape for one or more {@link Mesh}es. * * * {@link ReadableGeometry} is a subclass that stores its data in both browser and GPU memory. Use ReadableGeometry when you need to keep the geometry arrays in browser memory. * * {@link VBOGeometry} is a subclass that stores its data solely in GPU memory. Use VBOGeometry when you need a lower memory footprint and don't need to keep the geometry data in browser memory. */ class Geometry extends Component { /** @private */ get type() { return "Geometry"; } /** @private */ get isGeometry() { return true; } constructor(owner, cfg = {}) { super(owner, cfg); stats.memory.meshes++; } destroy() { super.destroy(); stats.memory.meshes--; } } const ids$4 = new Map$1({}); /** * @desc Represents a chunk of state changes applied by the {@link Scene}'s renderer while it renders a frame. * * * Contains properties that represent the state changes. * * Has a unique automatically-generated numeric ID, which the renderer can use to sort these, in order to avoid applying redundant state changes for each frame. * * Initialize your own properties on a RenderState via its constructor. * * @private */ class RenderState { constructor(cfg) { /** The RenderState's ID, unique within the renderer. @property id @type {Number} @final */ this.id = ids$4.addItem({}); for (const key in cfg) { if (cfg.hasOwnProperty(key)) { this[key] = cfg[key]; } } } /** Destroys this RenderState. */ destroy() { ids$4.removeItem(this.id); } } /** * @desc Represents a WebGL ArrayBuffer. * * @private */ class ArrayBuf { constructor(gl, type, data, numItems, itemSize, usage, normalized, stride, offset) { this._gl = gl; this.type = type; this.allocated = false; this.ConstructorType = data.constructor; switch (data.constructor) { case Uint8Array: this.itemType = gl.UNSIGNED_BYTE; this.itemByteSize = 1; break; case Int8Array: this.itemType = gl.BYTE; this.itemByteSize = 1; break; case Uint16Array: this.itemType = gl.UNSIGNED_SHORT; this.itemByteSize = 2; break; case Int16Array: this.itemType = gl.SHORT; this.itemByteSize = 2; break; case Uint32Array: this.itemType = gl.UNSIGNED_INT; this.itemByteSize = 4; break; case Int32Array: this.itemType = gl.INT; this.itemByteSize = 4; break; default: this.itemType = gl.FLOAT; this.itemByteSize = 4; } this.usage = usage; this.length = 0; this.dataLength = numItems; this.numItems = 0; this.itemSize = itemSize; this.normalized = !!normalized; this.stride = stride || 0; this.offset = offset || 0; this._allocate(data); } _allocate(data) { this.allocated = false; this._handle = this._gl.createBuffer(); if (!this._handle) { throw "Failed to allocate WebGL ArrayBuffer"; } if (this._handle) { this._gl.bindBuffer(this.type, this._handle); this._gl.bufferData(this.type, data.length > this.dataLength ? data.slice(0, this.dataLength) : data, this.usage); this._gl.bindBuffer(this.type, null); this.length = data.length; this.numItems = this.length / this.itemSize; this.allocated = true; } } setData(data, offset) { if (!this.allocated) { return; } if (data.length + (offset || 0) > this.length) { // Needs reallocation this.destroy(); this._allocate(data); } else { // No reallocation needed this._gl.bindBuffer(this.type, this._handle); if (offset || offset === 0) { this._gl.bufferSubData(this.type, offset * this.itemByteSize, data); } else { this._gl.bufferData(this.type, data, this.usage); } this._gl.bindBuffer(this.type, null); } } getData(baseIndex = 0, length = (this.numItems) - baseIndex) { if (!this.allocated) { return []; } const array = new (this.ConstructorType)(length * this.itemSize); this._gl.bindBuffer(this.type, this._handle); this._gl.getBufferSubData( this.type, baseIndex * this.itemByteSize * this.itemSize, array, 0, length * this.itemSize ); return array; } bind() { if (!this.allocated) { return; } this._gl.bindBuffer(this.type, this._handle); } unbind() { if (!this.allocated) { return; } this._gl.bindBuffer(this.type, null); } destroy() { if (!this.allocated) { return; } this._gl.deleteBuffer(this._handle); this._handle = null; this.allocated = false; } } /** * @private */ var buildEdgeIndices = (function () { const uniquePositions = []; const indicesLookup = []; const indicesReverseLookup = []; const weldedIndices = []; // TODO: Optimize with caching, but need to cater to both compressed and uncompressed positions const faces = []; let numFaces = 0; const compa = new Uint16Array(3); const compb = new Uint16Array(3); const compc = new Uint16Array(3); const a = math.vec3(); const b = math.vec3(); const c = math.vec3(); const cb = math.vec3(); const ab = math.vec3(); const cross = math.vec3(); const normal = math.vec3(); function weldVertices(positions, indices) { const positionsMap = {}; // Hashmap for looking up vertices by position coordinates (and making sure they are unique) let vx; let vy; let vz; let key; const precisionPoints = 4; // number of decimal points, e.g. 4 for epsilon of 0.0001 const precision = Math.pow(10, precisionPoints); let i; let len; let lenUniquePositions = 0; for (i = 0, len = positions.length; i < len; i += 3) { vx = positions[i]; vy = positions[i + 1]; vz = positions[i + 2]; key = Math.round(vx * precision) + '_' + Math.round(vy * precision) + '_' + Math.round(vz * precision); if (positionsMap[key] === undefined) { positionsMap[key] = lenUniquePositions / 3; uniquePositions[lenUniquePositions++] = vx; uniquePositions[lenUniquePositions++] = vy; uniquePositions[lenUniquePositions++] = vz; } indicesLookup[i / 3] = positionsMap[key]; } for (i = 0, len = indices.length; i < len; i++) { weldedIndices[i] = indicesLookup[indices[i]]; indicesReverseLookup[weldedIndices[i]] = indices[i]; } } function buildFaces(numIndices, positionsDecodeMatrix) { numFaces = 0; for (let i = 0, len = numIndices; i < len; i += 3) { const ia = ((weldedIndices[i]) * 3); const ib = ((weldedIndices[i + 1]) * 3); const ic = ((weldedIndices[i + 2]) * 3); if (positionsDecodeMatrix) { compa[0] = uniquePositions[ia]; compa[1] = uniquePositions[ia + 1]; compa[2] = uniquePositions[ia + 2]; compb[0] = uniquePositions[ib]; compb[1] = uniquePositions[ib + 1]; compb[2] = uniquePositions[ib + 2]; compc[0] = uniquePositions[ic]; compc[1] = uniquePositions[ic + 1]; compc[2] = uniquePositions[ic + 2]; // Decode math.decompressPosition(compa, positionsDecodeMatrix, a); math.decompressPosition(compb, positionsDecodeMatrix, b); math.decompressPosition(compc, positionsDecodeMatrix, c); } else { a[0] = uniquePositions[ia]; a[1] = uniquePositions[ia + 1]; a[2] = uniquePositions[ia + 2]; b[0] = uniquePositions[ib]; b[1] = uniquePositions[ib + 1]; b[2] = uniquePositions[ib + 2]; c[0] = uniquePositions[ic]; c[1] = uniquePositions[ic + 1]; c[2] = uniquePositions[ic + 2]; } math.subVec3(c, b, cb); math.subVec3(a, b, ab); math.cross3Vec3(cb, ab, cross); math.normalizeVec3(cross, normal); const face = faces[numFaces] || (faces[numFaces] = {normal: math.vec3()}); face.normal[0] = normal[0]; face.normal[1] = normal[1]; face.normal[2] = normal[2]; numFaces++; } } return function (positions, indices, positionsDecodeMatrix, edgeThreshold) { weldVertices(positions, indices); buildFaces(indices.length, positionsDecodeMatrix); const edgeIndices = []; const thresholdDot = Math.cos(math.DEGTORAD * edgeThreshold); const edges = {}; let edge1; let edge2; let index1; let index2; let key; let largeIndex = false; let edge; let normal1; let normal2; let dot; let ia; let ib; for (let i = 0, len = indices.length; i < len; i += 3) { const faceIndex = i / 3; for (let j = 0; j < 3; j++) { edge1 = weldedIndices[i + j]; edge2 = weldedIndices[i + ((j + 1) % 3)]; index1 = Math.min(edge1, edge2); index2 = Math.max(edge1, edge2); key = index1 + "," + index2; if (edges[key] === undefined) { edges[key] = { index1: index1, index2: index2, face1: faceIndex, face2: undefined }; } else { edges[key].face2 = faceIndex; } } } for (key in edges) { edge = edges[key]; // an edge is only rendered if the angle (in degrees) between the face normals of the adjoining faces exceeds this value. default = 1 degree. if (edge.face2 !== undefined) { normal1 = faces[edge.face1].normal; normal2 = faces[edge.face2].normal; dot = math.dotVec3(normal1, normal2); if (dot > thresholdDot) { continue; } } ia = indicesReverseLookup[edge.index1]; ib = indicesReverseLookup[edge.index2]; if (!largeIndex && ia > 65535 || ib > 65535) { largeIndex = true; } edgeIndices.push(ia); edgeIndices.push(ib); } return (largeIndex) ? new Uint32Array(edgeIndices) : new Uint16Array(edgeIndices); }; })(); /** * Private geometry compression and decompression utilities. */ /** * @private * @param array * @returns {{min: Float32Array, max: Float32Array}} */ function getPositionsBounds(array) { const min = new Float32Array(3); const max = new Float32Array(3); let i, j; for (i = 0; i < 3; i++) { min[i] = Number.MAX_VALUE; max[i] = -Number.MAX_VALUE; } for (i = 0; i < array.length; i += 3) { for (j = 0; j < 3; j++) { min[j] = Math.min(min[j], array[i + j]); max[j] = Math.max(max[j], array[i + j]); } } return { min: min, max: max }; } const createPositionsDecodeMatrix$1 = (function () { const translate = math.mat4(); const scale = math.mat4(); return function (aabb, positionsDecodeMatrix) { positionsDecodeMatrix = positionsDecodeMatrix || math.mat4(); const xmin = aabb[0]; const ymin = aabb[1]; const zmin = aabb[2]; const xwid = aabb[3] - xmin; const ywid = aabb[4] - ymin; const zwid = aabb[5] - zmin; const maxInt = 65535; math.identityMat4(translate); math.translationMat4v(aabb, translate); math.identityMat4(scale); math.scalingMat4v([xwid / maxInt, ywid / maxInt, zwid / maxInt], scale); math.mulMat4(translate, scale, positionsDecodeMatrix); return positionsDecodeMatrix; }; })(); /** * @private */ var compressPositions = (function () { // http://cg.postech.ac.kr/research/mesh_comp_mobile/mesh_comp_mobile_conference.pdf const translate = math.mat4(); const scale = math.mat4(); return function (array, min, max) { const quantized = new Uint16Array(array.length); const multiplier = new Float32Array([ max[0] !== min[0] ? 65535 / (max[0] - min[0]) : 0, max[1] !== min[1] ? 65535 / (max[1] - min[1]) : 0, max[2] !== min[2] ? 65535 / (max[2] - min[2]) : 0 ]); let i; for (i = 0; i < array.length; i += 3) { quantized[i + 0] = Math.max(0, Math.min(65535, Math.floor((array[i + 0] - min[0]) * multiplier[0]))); quantized[i + 1] = Math.max(0, Math.min(65535, Math.floor((array[i + 1] - min[1]) * multiplier[1]))); quantized[i + 2] = Math.max(0, Math.min(65535, Math.floor((array[i + 2] - min[2]) * multiplier[2]))); } math.identityMat4(translate); math.translationMat4v(min, translate); math.identityMat4(scale); math.scalingMat4v([ (max[0] - min[0]) / 65535, (max[1] - min[1]) / 65535, (max[2] - min[2]) / 65535 ], scale); const decodeMat = math.mulMat4(translate, scale, math.identityMat4()); return { quantized: quantized, decodeMatrix: decodeMat }; }; })(); function compressPosition(p, aabb, q) { const multiplier = new Float32Array([ aabb[3] !== aabb[0] ? 65535 / (aabb[3] - aabb[0]) : 0, aabb[4] !== aabb[1] ? 65535 / (aabb[4] - aabb[1]) : 0, aabb[5] !== aabb[2] ? 65535 / (aabb[5] - aabb[2]) : 0 ]); q[0] = Math.max(0, Math.min(65535, Math.floor((p[0] - aabb[0]) * multiplier[0]))); q[1] = Math.max(0, Math.min(65535, Math.floor((p[1] - aabb[1]) * multiplier[1]))); q[2] = Math.max(0, Math.min(65535, Math.floor((p[2] - aabb[2]) * multiplier[2]))); } function decompressPosition(position, decodeMatrix, dest) { dest[0] = position[0] * decodeMatrix[0] + decodeMatrix[12]; dest[1] = position[1] * decodeMatrix[5] + decodeMatrix[13]; dest[2] = position[2] * decodeMatrix[10] + decodeMatrix[14]; return dest; } function decompressAABB(aabb, decodeMatrix, dest = aabb) { dest[0] = aabb[0] * decodeMatrix[0] + decodeMatrix[12]; dest[1] = aabb[1] * decodeMatrix[5] + decodeMatrix[13]; dest[2] = aabb[2] * decodeMatrix[10] + decodeMatrix[14]; dest[3] = aabb[3] * decodeMatrix[0] + decodeMatrix[12]; dest[4] = aabb[4] * decodeMatrix[5] + decodeMatrix[13]; dest[5] = aabb[5] * decodeMatrix[10] + decodeMatrix[14]; return dest; } /** * @private */ function decompressPositions(positions, decodeMatrix, dest = new Float32Array(positions.length)) { for (let i = 0, len = positions.length; i < len; i += 3) { dest[i + 0] = positions[i + 0] * decodeMatrix[0] + decodeMatrix[12]; dest[i + 1] = positions[i + 1] * decodeMatrix[5] + decodeMatrix[13]; dest[i + 2] = positions[i + 2] * decodeMatrix[10] + decodeMatrix[14]; } return dest; } //--------------- UVs -------------------------------------------------------------------------------------------------- /** * @private * @param array * @returns {{min: Float32Array, max: Float32Array}} */ function getUVBounds(array) { const min = new Float32Array(2); const max = new Float32Array(2); let i, j; for (i = 0; i < 2; i++) { min[i] = Number.MAX_VALUE; max[i] = -Number.MAX_VALUE; } for (i = 0; i < array.length; i += 2) { for (j = 0; j < 2; j++) { min[j] = Math.min(min[j], array[i + j]); max[j] = Math.max(max[j], array[i + j]); } } return { min: min, max: max }; } /** * @private */ var compressUVs = (function () { const translate = math.mat3(); const scale = math.mat3(); return function (array, min, max) { const quantized = new Uint16Array(array.length); const multiplier = new Float32Array([ 65535 / (max[0] - min[0]), 65535 / (max[1] - min[1]) ]); let i; for (i = 0; i < array.length; i += 2) { quantized[i + 0] = Math.max(0, Math.min(65535, Math.floor((array[i + 0] - min[0]) * multiplier[0]))); quantized[i + 1] = Math.max(0, Math.min(65535, Math.floor((array[i + 1] - min[1]) * multiplier[1]))); } math.identityMat3(translate); math.translationMat3v(min, translate); math.identityMat3(scale); math.scalingMat3v([ (max[0] - min[0]) / 65535, (max[1] - min[1]) / 65535 ], scale); const decodeMat = math.mulMat3(translate, scale, math.identityMat3()); return { quantized: quantized, decodeMatrix: decodeMat }; }; })(); //--------------- Normals ---------------------------------------------------------------------------------------------- /** * @private */ function compressNormals(array) { // http://jcgt.org/published/0003/02/01/ // Note: three elements for each encoded normal, in which the last element in each triplet is redundant. // This is to work around a mysterious WebGL issue where 2-element normals just wouldn't work in the shader :/ const encoded = new Int8Array(array.length); let oct, dec, best, currentCos, bestCos; for (let i = 0; i < array.length; i += 3) { // Test various combinations of ceil and floor // to minimize rounding errors best = oct = octEncodeVec3$1(array, i, "floor", "floor"); dec = octDecodeVec2$1(oct); currentCos = bestCos = dot$1(array, i, dec); oct = octEncodeVec3$1(array, i, "ceil", "floor"); dec = octDecodeVec2$1(oct); currentCos = dot$1(array, i, dec); if (currentCos > bestCos) { best = oct; bestCos = currentCos; } oct = octEncodeVec3$1(array, i, "floor", "ceil"); dec = octDecodeVec2$1(oct); currentCos = dot$1(array, i, dec); if (currentCos > bestCos) { best = oct; bestCos = currentCos; } oct = octEncodeVec3$1(array, i, "ceil", "ceil"); dec = octDecodeVec2$1(oct); currentCos = dot$1(array, i, dec); if (currentCos > bestCos) { best = oct; bestCos = currentCos; } encoded[i] = best[0]; encoded[i + 1] = best[1]; } return encoded; } /** * @private */ function octEncodeVec3$1(array, i, xfunc, yfunc) { // Oct-encode single normal vector in 2 bytes let x = array[i] / (Math.abs(array[i]) + Math.abs(array[i + 1]) + Math.abs(array[i + 2])); let y = array[i + 1] / (Math.abs(array[i]) + Math.abs(array[i + 1]) + Math.abs(array[i + 2])); if (array[i + 2] < 0) { let tempx = (1 - Math.abs(y)) * (x >= 0 ? 1 : -1); let tempy = (1 - Math.abs(x)) * (y >= 0 ? 1 : -1); x = tempx; y = tempy; } return new Int8Array([ Math[xfunc](x * 127.5 + (x < 0 ? -1 : 0)), Math[yfunc](y * 127.5 + (y < 0 ? -1 : 0)) ]); } /** * Decode an oct-encoded normal */ function octDecodeVec2$1(oct) { let x = oct[0]; let y = oct[1]; x /= x < 0 ? 127 : 128; y /= y < 0 ? 127 : 128; const z = 1 - Math.abs(x) - Math.abs(y); if (z < 0) { x = (1 - Math.abs(y)) * (x >= 0 ? 1 : -1); y = (1 - Math.abs(x)) * (y >= 0 ? 1 : -1); } const length = Math.sqrt(x * x + y * y + z * z); return [ x / length, y / length, z / length ]; } /** * Dot product of a normal in an array against a candidate decoding * @private */ function dot$1(array, i, vec3) { return array[i] * vec3[0] + array[i + 1] * vec3[1] + array[i + 2] * vec3[2]; } /** * @private */ function decompressUV(uv, decodeMatrix, dest) { dest[0] = uv[0] * decodeMatrix[0] + decodeMatrix[6]; dest[1] = uv[1] * decodeMatrix[4] + decodeMatrix[7]; } /** * @private */ function decompressUVs(uvs, decodeMatrix, dest = new Float32Array(uvs.length)) { for (let i = 0, len = uvs.length; i < len; i += 3) { dest[i + 0] = uvs[i + 0] * decodeMatrix[0] + decodeMatrix[6]; dest[i + 1] = uvs[i + 1] * decodeMatrix[4] + decodeMatrix[7]; } return dest; } /** * @private */ function decompressNormal(oct, result) { let x = oct[0]; let y = oct[1]; x = (2 * x + 1) / 255; y = (2 * y + 1) / 255; const z = 1 - Math.abs(x) - Math.abs(y); if (z < 0) { x = (1 - Math.abs(y)) * (x >= 0 ? 1 : -1); y = (1 - Math.abs(x)) * (y >= 0 ? 1 : -1); } const length = Math.sqrt(x * x + y * y + z * z); result[0] = x / length; result[1] = y / length; result[2] = z / length; return result; } /** * @private */ function decompressNormals(octs, result) { for (let i = 0, j = 0, len = octs.length; i < len; i += 2) { let x = octs[i + 0]; let y = octs[i + 1]; x = (2 * x + 1) / 255; y = (2 * y + 1) / 255; const z = 1 - Math.abs(x) - Math.abs(y); if (z < 0) { x = (1 - Math.abs(y)) * (x >= 0 ? 1 : -1); y = (1 - Math.abs(x)) * (y >= 0 ? 1 : -1); } const length = Math.sqrt(x * x + y * y + z * z); result[j + 0] = x / length; result[j + 1] = y / length; result[j + 2] = z / length; j += 3; } return result; } /** * @private */ const geometryCompressionUtils = { getPositionsBounds: getPositionsBounds, createPositionsDecodeMatrix: createPositionsDecodeMatrix$1, compressPositions: compressPositions, compressPosition:compressPosition, decompressPositions: decompressPositions, decompressPosition: decompressPosition, decompressAABB: decompressAABB, getUVBounds: getUVBounds, compressUVs: compressUVs, decompressUVs: decompressUVs, decompressUV: decompressUV, compressNormals: compressNormals, decompressNormals: decompressNormals, decompressNormal: decompressNormal }; const memoryStats$1 = stats.memory; const tempAABB$3 = math.AABB3(); /** * @desc A {@link Geometry} that keeps its geometry data in both browser and GPU memory. * * ReadableGeometry uses more memory than {@link VBOGeometry}, which only stores its geometry data in GPU memory. * * ## Usage * * Creating a {@link Mesh} with a ReadableGeometry that defines a single triangle, plus a {@link PhongMaterial} with diffuse {@link Texture}: * * [[Run this example](/examples/index.html#geometry_ReadableGeometry)] * * ````javascript * import {Viewer, Mesh, ReadableGeometry, PhongMaterial, Texture} from "xeokit-sdk.es.js"; * * const viewer = new Viewer({ * canvasId: "myCanvas" * }); * * const myMesh = new Mesh(viewer.scene, { * geometry: new ReadableGeometry(viewer.scene, { * primitive: "triangles", * positions: [0.0, 3, 0.0, -3, -3, 0.0, 3, -3, 0.0], * normals: [0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0], * uv: [0.0, 0.0, 0.5, 1.0, 1.0, 0.0], * indices: [0, 1, 2] * }), * material: new PhongMaterial(viewer.scene, { * diffuseMap: new Texture(viewer.scene, { * src: "textures/diffuse/uvGrid2.jpg" * }), * backfaces: true * }) * }); * * // Get geometry data from browser memory: * * const positions = myMesh.geometry.positions; // Flat arrays * const normals = myMesh.geometry.normals; * const uv = myMesh.geometry.uv; * const indices = myMesh.geometry.indices; * * ```` */ class ReadableGeometry extends Geometry { /** @private */ get type() { return "ReadableGeometry"; } /** * @private * @returns {Boolean} */ get isReadableGeometry() { return true; } /** * @class ReadableGeometry @module xeokit @submodule geometry @constructor @param {Component} owner Owner component. When destroyed, the owner will destroy this component as well. @param {*} [cfg] Configs @param {String} [cfg.id] Optional ID, unique among all components in the parent {@link Scene}, generated automatically when omitted. @param {String:Object} [cfg.meta] Optional map of user-defined metadata to attach to this Geometry. @param [cfg.primitive="triangles"] {String} The primitive type. Accepted values are 'points', 'lines', 'line-loop', 'line-strip', 'triangles', 'triangle-strip' and 'triangle-fan'. @param [cfg.positions] {Number[]} Positions array. @param [cfg.normals] {Number[]} Vertex normal vectors array. @param [cfg.uv] {Number[]} UVs array. @param [cfg.colors] {Number[]} Vertex colors. @param [cfg.indices] {Number[]} Indices array. @param [cfg.autoVertexNormals=false] {Boolean} Set true to automatically generate normal vectors from the positions and indices, if those are supplied. @param [cfg.compressGeometry=false] {Boolean} Stores positions, colors, normals and UVs in compressGeometry and oct-encoded formats for reduced memory footprint and GPU bus usage. @param [cfg.edgeThreshold=10] {Number} When a {@link Mesh} renders this Geometry as wireframe, this indicates the threshold angle (in degrees) between the face normals of adjacent triangles below which the edge is discarded. @extends Component * @param owner * @param cfg */ constructor(owner, cfg = {}) { super(owner, cfg); this._state = new RenderState({ // Arrays for emphasis effects are got from xeokit.Geometry friend methods compressGeometry: !!cfg.compressGeometry, primitive: null, // WebGL enum primitiveName: null, // String positions: null, // Uint16Array when compressGeometry == true, else Float32Array normals: null, // Uint8Array when compressGeometry == true, else Float32Array colors: null, uv: null, // Uint8Array when compressGeometry == true, else Float32Array indices: null, positionsDecodeMatrix: null, // Set when compressGeometry == true uvDecodeMatrix: null, // Set when compressGeometry == true positionsBuf: null, normalsBuf: null, colorsbuf: null, uvBuf: null, indicesBuf: null, hash: "" }); this._numTriangles = 0; this._edgeThreshold = cfg.edgeThreshold || 10.0; // Lazy-generated VBOs this._edgeIndicesBuf = null; this._pickTrianglePositionsBuf = null; this._pickTriangleColorsBuf = null; // Local-space Boundary3D this._aabbDirty = true; this._boundingSphere = true; this._aabb = null; this._aabbDirty = true; this._obb = null; this._obbDirty = true; const state = this._state; const gl = this.scene.canvas.gl; // Primitive type cfg.primitive = cfg.primitive || "triangles"; switch (cfg.primitive) { case "points": state.primitive = gl.POINTS; state.primitiveName = cfg.primitive; break; case "lines": state.primitive = gl.LINES; state.primitiveName = cfg.primitive; break; case "line-loop": state.primitive = gl.LINE_LOOP; state.primitiveName = cfg.primitive; break; case "line-strip": state.primitive = gl.LINE_STRIP; state.primitiveName = cfg.primitive; break; case "triangles": state.primitive = gl.TRIANGLES; state.primitiveName = cfg.primitive; break; case "triangle-strip": state.primitive = gl.TRIANGLE_STRIP; state.primitiveName = cfg.primitive; break; case "triangle-fan": state.primitive = gl.TRIANGLE_FAN; state.primitiveName = cfg.primitive; break; default: this.error("Unsupported value for 'primitive': '" + cfg.primitive + "' - supported values are 'points', 'lines', 'line-loop', 'line-strip', 'triangles', " + "'triangle-strip' and 'triangle-fan'. Defaulting to 'triangles'."); state.primitive = gl.TRIANGLES; state.primitiveName = cfg.primitive; } if (cfg.positions) { if (this._state.compressGeometry) { const bounds = geometryCompressionUtils.getPositionsBounds(cfg.positions); const result = geometryCompressionUtils.compressPositions(cfg.positions, bounds.min, bounds.max); state.positions = result.quantized; state.positionsDecodeMatrix = result.decodeMatrix; } else { state.positions = cfg.positions.constructor === Float32Array ? cfg.positions : new Float32Array(cfg.positions); } } if (cfg.colors) { state.colors = cfg.colors.constructor === Float32Array ? cfg.colors : new Float32Array(cfg.colors); } if (cfg.uv) { if (this._state.compressGeometry) { const bounds = geometryCompressionUtils.getUVBounds(cfg.uv); const result = geometryCompressionUtils.compressUVs(cfg.uv, bounds.min, bounds.max); state.uv = result.quantized; state.uvDecodeMatrix = result.decodeMatrix; } else { state.uv = cfg.uv.constructor === Float32Array ? cfg.uv : new Float32Array(cfg.uv); } } if (cfg.normals) { if (this._state.compressGeometry) { state.normals = geometryCompressionUtils.compressNormals(cfg.normals); } else { state.normals = cfg.normals.constructor === Float32Array ? cfg.normals : new Float32Array(cfg.normals); } } if (cfg.indices) { state.indices = (cfg.indices.constructor === Uint32Array || cfg.indices.constructor === Uint16Array) ? cfg.indices : new Uint32Array(cfg.indices); if (this._state.primitiveName === "triangles") { this._numTriangles = (cfg.indices.length / 3); } } this._buildHash(); memoryStats$1.meshes++; this._buildVBOs(); } _buildVBOs() { const state = this._state; const gl = this.scene.canvas.gl; if (state.indices) { state.indicesBuf = new ArrayBuf(gl, gl.ELEMENT_ARRAY_BUFFER, state.indices, state.indices.length, 1, gl.STATIC_DRAW); memoryStats$1.indices += state.indicesBuf.numItems; } if (state.positions) { state.positionsBuf = new ArrayBuf(gl, gl.ARRAY_BUFFER, state.positions, state.positions.length, 3, gl.STATIC_DRAW); memoryStats$1.positions += state.positionsBuf.numItems; } if (state.normals) { let normalized = state.compressGeometry; state.normalsBuf = new ArrayBuf(gl, gl.ARRAY_BUFFER, state.normals, state.normals.length, 3, gl.STATIC_DRAW, normalized); memoryStats$1.normals += state.normalsBuf.numItems; } if (state.colors) { state.colorsBuf = new ArrayBuf(gl, gl.ARRAY_BUFFER, state.colors, state.colors.length, 4, gl.STATIC_DRAW); memoryStats$1.colors += state.colorsBuf.numItems; } if (state.uv) { state.uvBuf = new ArrayBuf(gl, gl.ARRAY_BUFFER, state.uv, state.uv.length, 2, gl.STATIC_DRAW); memoryStats$1.uvs += state.uvBuf.numItems; } } _buildHash() { const state = this._state; const hash = ["/g"]; hash.push("/" + state.primitive + ";"); if (state.positions) { hash.push("p"); } if (state.colors) { hash.push("c"); } if (state.normals || state.autoVertexNormals) { hash.push("n"); } if (state.uv) { hash.push("u"); } if (state.compressGeometry) { hash.push("cp"); } hash.push(";"); state.hash = hash.join(""); } _getEdgeIndices() { if (!this._edgeIndicesBuf) { this._buildEdgeIndices(); } return this._edgeIndicesBuf; } _getPickTrianglePositions() { if (!this._pickTrianglePositionsBuf) { this._buildPickTriangleVBOs(); } return this._pickTrianglePositionsBuf; } _getPickTriangleColors() { if (!this._pickTriangleColorsBuf) { this._buildPickTriangleVBOs(); } return this._pickTriangleColorsBuf; } _buildEdgeIndices() { // FIXME: Does not adjust indices after other objects are deleted from vertex buffer!! const state = this._state; if (!state.positions || !state.indices) { return; } const gl = this.scene.canvas.gl; const edgeIndices = buildEdgeIndices(state.positions, state.indices, state.positionsDecodeMatrix, this._edgeThreshold); this._edgeIndicesBuf = new ArrayBuf(gl, gl.ELEMENT_ARRAY_BUFFER, edgeIndices, edgeIndices.length, 1, gl.STATIC_DRAW); memoryStats$1.indices += this._edgeIndicesBuf.numItems; } _buildPickTriangleVBOs() { // Builds positions and indices arrays that allow each triangle to have a unique color const state = this._state; if (!state.positions || !state.indices) { return; } const gl = this.scene.canvas.gl; const arrays = math.buildPickTriangles(state.positions, state.indices, state.compressGeometry); const positions = arrays.positions; const colors = arrays.colors; this._pickTrianglePositionsBuf = new ArrayBuf(gl, gl.ARRAY_BUFFER, positions, positions.length, 3, gl.STATIC_DRAW); this._pickTriangleColorsBuf = new ArrayBuf(gl, gl.ARRAY_BUFFER, colors, colors.length, 4, gl.STATIC_DRAW, true); memoryStats$1.positions += this._pickTrianglePositionsBuf.numItems; memoryStats$1.colors += this._pickTriangleColorsBuf.numItems; } _buildPickVertexVBOs() { // var state = this._state; // if (!state.positions || !state.indices) { // return; // } // var gl = this.scene.canvas.gl; // var arrays = math.buildPickVertices(state.positions, state.indices, state.compressGeometry); // var pickVertexPositions = arrays.positions; // var pickColors = arrays.colors; // this._pickVertexPositionsBuf = new xeokit.renderer.ArrayBuf(gl, gl.ARRAY_BUFFER, pickVertexPositions, pickVertexPositions.length, 3, gl.STATIC_DRAW); // this._pickVertexColorsBuf = new xeokit.renderer.ArrayBuf(gl, gl.ARRAY_BUFFER, pickColors, pickColors.length, 4, gl.STATIC_DRAW, true); // memoryStats.positions += this._pickVertexPositionsBuf.numItems; // memoryStats.colors += this._pickVertexColorsBuf.numItems; } _webglContextLost() { if (this._sceneVertexBufs) { this._sceneVertexBufs.webglContextLost(); } } _webglContextRestored() { if (this._sceneVertexBufs) { this._sceneVertexBufs.webglContextRestored(); } this._buildVBOs(); this._edgeIndicesBuf = null; this._pickVertexPositionsBuf = null; this._pickTrianglePositionsBuf = null; this._pickTriangleColorsBuf = null; this._pickVertexPositionsBuf = null; this._pickVertexColorsBuf = null; } /** * Gets the Geometry's primitive type. Valid types are: 'points', 'lines', 'line-loop', 'line-strip', 'triangles', 'triangle-strip' and 'triangle-fan'. @property primitive @default "triangles" @type {String} */ get primitive() { return this._state.primitiveName; } /** Indicates if this Geometry is quantized. Compression is an internally-performed optimization which stores positions, colors, normals and UVs in quantized and oct-encoded formats for reduced memory footprint and GPU bus usage. Quantized geometry may not be updated. @property compressGeometry @default false @type {Boolean} @final */ get compressGeometry() { return this._state.compressGeometry; } /** The Geometry's vertex positions. @property positions @default null @type {Number[]} */ get positions() { if (!this._state.positions) { return null; } if (!this._state.compressGeometry) { return this._state.positions; } if (!this._decompressedPositions) { this._decompressedPositions = new Float32Array(this._state.positions.length); geometryCompressionUtils.decompressPositions(this._state.positions, this._state.positionsDecodeMatrix, this._decompressedPositions); } return this._decompressedPositions; } set positions(newPositions) { const state = this._state; const positions = state.positions; if (!positions) { this.error("can't update geometry positions - geometry has no positions"); return; } if (positions.length !== newPositions.length) { this.error("can't update geometry positions - new positions are wrong length"); return; } if (this._state.compressGeometry) { const bounds = geometryCompressionUtils.getPositionsBounds(newPositions); const result = geometryCompressionUtils.compressPositions(newPositions, bounds.min, bounds.max); newPositions = result.quantized; // TODO: Copy in-place state.positionsDecodeMatrix = result.decodeMatrix; } positions.set(newPositions); if (state.positionsBuf) { state.positionsBuf.setData(positions); } this._setAABBDirty(); this.glRedraw(); } /** The Geometry's vertex normals. @property normals @default null @type {Number[]} */ get normals() { if (!this._state.normals) { return; } if (!this._state.compressGeometry) { return this._state.normals; } if (!this._decompressedNormals) { const lenCompressed = this._state.normals.length; const lenDecompressed = lenCompressed + (lenCompressed / 2); // 2 -> 3 this._decompressedNormals = new Float32Array(lenDecompressed); geometryCompressionUtils.decompressNormals(this._state.normals, this._decompressedNormals); } return this._decompressedNormals; } set normals(newNormals) { if (this._state.compressGeometry) { this.error("can't update geometry normals - quantized geometry is immutable"); // But will be eventually return; } const state = this._state; const normals = state.normals; if (!normals) { this.error("can't update geometry normals - geometry has no normals"); return; } if (normals.length !== newNormals.length) { this.error("can't update geometry normals - new normals are wrong length"); return; } normals.set(newNormals); if (state.normalsBuf) { state.normalsBuf.setData(normals); } this.glRedraw(); } /** The Geometry's UV coordinates. @property uv @default null @type {Number[]} */ get uv() { if (!this._state.uv) { return null; } if (!this._state.compressGeometry) { return this._state.uv; } if (!this._decompressedUV) { this._decompressedUV = new Float32Array(this._state.uv.length); geometryCompressionUtils.decompressUVs(this._state.uv, this._state.uvDecodeMatrix, this._decompressedUV); } return this._decompressedUV; } set uv(newUV) { if (this._state.compressGeometry) { this.error("can't update geometry UVs - quantized geometry is immutable"); // But will be eventually return; } const state = this._state; const uv = state.uv; if (!uv) { this.error("can't update geometry UVs - geometry has no UVs"); return; } if (uv.length !== newUV.length) { this.error("can't update geometry UVs - new UVs are wrong length"); return; } uv.set(newUV); if (state.uvBuf) { state.uvBuf.setData(uv); } this.glRedraw(); } /** The Geometry's vertex colors. @property colors @default null @type {Number[]} */ get colors() { return this._state.colors; } set colors(newColors) { if (this._state.compressGeometry) { this.error("can't update geometry colors - quantized geometry is immutable"); // But will be eventually return; } const state = this._state; const colors = state.colors; if (!colors) { this.error("can't update geometry colors - geometry has no colors"); return; } if (colors.length !== newColors.length) { this.error("can't update geometry colors - new colors are wrong length"); return; } colors.set(newColors); if (state.colorsBuf) { state.colorsBuf.setData(colors); } this.glRedraw(); } /** The Geometry's indices. If ````xeokit.WEBGL_INFO.SUPPORTED_EXTENSIONS["OES_element_index_uint"]```` is true, then this can be a ````Uint32Array````, otherwise it needs to be a ````Uint16Array````. @property indices @default null @type Uint16Array | Uint32Array @final */ get indices() { return this._state.indices; } /** * Local-space axis-aligned 3D boundary (AABB) of this geometry. * * The AABB is represented by a six-element Float64Array containing the min/max extents of the * axis-aligned volume, ie. ````[xmin, ymin,zmin,xmax,ymax, zmax]````. * * @property aabb * @final * @type {Number[]} */ get aabb() { if (this._aabbDirty) { if (!this._aabb) { this._aabb = math.AABB3(); } math.positions3ToAABB3(this._state.positions, this._aabb, this._state.positionsDecodeMatrix); this._aabbDirty = false; } return this._aabb; } /** * Local-space oriented 3D boundary (OBB) of this geometry. * * The OBB is represented by a 32-element Float64Array containing the eight vertices of the box, * where each vertex is a homogeneous coordinate having [x,y,z,w] elements. * * @property obb * @final * @type {Number[]} */ get obb() { if (this._obbDirty) { if (!this._obb) { this._obb = math.OBB3(); } math.positions3ToAABB3(this._state.positions, tempAABB$3, this._state.positionsDecodeMatrix); math.AABB3ToOBB3(tempAABB$3, this._obb); this._obbDirty = false; } return this._obb; } _getMetrics() { if (! ("_metrics" in this)) { switch (this._state.primitiveName) { case "solid": case "surface": case "triangles": { const indices = this._state.indices; const positions = this._state.positions; const getPos = (i, out) => { const idx = indices[i] * 3; for (let j = 0; j < 3; ++j) { out[j] = positions[idx + j]; } return out; }; const tmp = [ math.vec3(), math.vec3(), math.vec3(), math.vec3() ]; let totalArea = 0; const centroid = math.vec3([ 0, 0, 0 ]); for (let i = 0; i < indices.length; i += 3) { const v0 = getPos(i, tmp[0]); const v1 = getPos(i+1, tmp[1]); const v2 = getPos(i+2, tmp[2]); math.addVec3(v0, v1, tmp[3]); math.addVec3(v2, tmp[3], tmp[3]); const faceArea = math.lenVec3( math.cross3Vec3( math.subVec3(v1, v0, tmp[1]), math.subVec3(v2, v0, tmp[2]), tmp[0])) / 2; totalArea += faceArea; math.mulVec3Scalar(tmp[3], faceArea, tmp[3]); math.addVec3(centroid, tmp[3], centroid); } this._metrics = { surfaceArea: totalArea, centroid: math.mulVec3Scalar(centroid, 1 / totalArea / 3, centroid) }; break; } default: this._metrics = { surfaceArea: 0 }; break; } } return this._metrics; } /** * Returns the surface area of this Mesh. * @returns {number} */ get surfaceArea() { return this._getMetrics().surfaceArea; } /** * Returns the centroid of this Mesh. * @returns {number} */ get centroid() { return this._getMetrics().centroid; } /** * Approximate number of triangles in this ReadableGeometry. * * Will be zero if {@link ReadableGeometry#primitive} is not 'triangles', 'triangle-strip' or 'triangle-fan'. * * @type {Number} */ get numTriangles() { return this._numTriangles; } _setAABBDirty() { if (this._aabbDirty) { return; } this._aabbDirty = true; this._aabbDirty = true; this._obbDirty = true; } _getState() { return this._state; } /** * Destroys this ReadableGeometry */ destroy() { super.destroy(); const state = this._state; if (state.indicesBuf) { state.indicesBuf.destroy(); } if (state.positionsBuf) { state.positionsBuf.destroy(); } if (state.normalsBuf) { state.normalsBuf.destroy(); } if (state.uvBuf) { state.uvBuf.destroy(); } if (state.colorsBuf) { state.colorsBuf.destroy(); } if (this._edgeIndicesBuf) { this._edgeIndicesBuf.destroy(); } if (this._pickTrianglePositionsBuf) { this._pickTrianglePositionsBuf.destroy(); } if (this._pickTriangleColorsBuf) { this._pickTriangleColorsBuf.destroy(); } if (this._pickVertexPositionsBuf) { this._pickVertexPositionsBuf.destroy(); } if (this._pickVertexColorsBuf) { this._pickVertexColorsBuf.destroy(); } state.destroy(); memoryStats$1.meshes--; } } const memoryStats = stats.memory; const tempAABB$2 = math.AABB3(); /** * @desc A {@link Geometry} that keeps its geometry data solely in GPU memory, without retaining it in browser memory. * * VBOGeometry uses less memory than {@link ReadableGeometry}, which keeps its geometry data in both browser and GPU memory. * * ## Usage * * Creating a {@link Mesh} with a VBOGeometry that defines a single triangle, plus a {@link PhongMaterial} with diffuse {@link Texture}: * * [[Run this example](/examples/index.html#geometry_VBOGeometry)] * * ````javascript * import {Viewer, Mesh, VBOGeometry, PhongMaterial, Texture} from "xeokit-sdk.es.js"; * * const viewer = new Viewer({ * canvasId: "myCanvas" * }); * * new Mesh(viewer.scene, { * geometry: new VBOGeometry(viewer.scene, { * primitive: "triangles", * positions: [0.0, 3, 0.0, -3, -3, 0.0, 3, -3, 0.0], * normals: [0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0], * uv: [0.0, 0.0, 0.5, 1.0, 1.0, 0.0], * indices: [0, 1, 2] * }), * material: new PhongMaterial(viewer.scene, { * diffuseMap: new Texture(viewer.scene, { * src: "textures/diffuse/uvGrid2.jpg" * }), * backfaces: true * }) * }); * ```` */ class VBOGeometry extends Geometry { /** @private */ get type() { return "VBOGeometry"; } /** * @private * @returns {Boolean} */ get isVBOGeometry() { return true; } /** * @constructor * @param {Component} owner Owner component. When destroyed, the owner will destroy this component as well. * @param {*} [cfg] Configs * @param {String} [cfg.id] Optional ID, unique among all components in the parent {@link Scene}, generated automatically when omitted. * @param {String} [cfg.primitive="triangles"] The primitive type. Accepted values are 'points', 'lines', 'line-loop', 'line-strip', 'triangles', 'triangle-strip' and 'triangle-fan'. * @param {Number[]} [cfg.positions] Positions array. * @param {Number[]} [cfg.normals] Vertex normal vectors array. * @param {Number[]} [cfg.uv] UVs array. * @param {Number[]} [cfg.colors] Vertex colors. * @param {Number[]} [cfg.indices] Indices array. * @param {Number} [cfg.edgeThreshold=10] When autogenerating edges for supporting {@link Drawable#edges}, this indicates the threshold angle (in degrees) between the face normals of adjacent triangles below which the edge is discarded. */ constructor(owner, cfg = {}) { super(owner, cfg); this._state = new RenderState({ // Arrays for emphasis effects are got from xeokit.GeometryLite friend methods compressGeometry: true, primitive: null, // WebGL enum primitiveName: null, // String positionsDecodeMatrix: null, // Set when compressGeometry == true uvDecodeMatrix: null, // Set when compressGeometry == true positionsBuf: null, normalsBuf: null, colorsbuf: null, uvBuf: null, indicesBuf: null, hash: "" }); this._numTriangles = 0; this._edgeThreshold = cfg.edgeThreshold || 10.0; this._aabb = null; this._obb = math.OBB3(); const state = this._state; const gl = this.scene.canvas.gl; cfg.primitive = cfg.primitive || "triangles"; switch (cfg.primitive) { case "points": state.primitive = gl.POINTS; state.primitiveName = cfg.primitive; break; case "lines": state.primitive = gl.LINES; state.primitiveName = cfg.primitive; break; case "line-loop": state.primitive = gl.LINE_LOOP; state.primitiveName = cfg.primitive; break; case "line-strip": state.primitive = gl.LINE_STRIP; state.primitiveName = cfg.primitive; break; case "triangles": state.primitive = gl.TRIANGLES; state.primitiveName = cfg.primitive; break; case "triangle-strip": state.primitive = gl.TRIANGLE_STRIP; state.primitiveName = cfg.primitive; break; case "triangle-fan": state.primitive = gl.TRIANGLE_FAN; state.primitiveName = cfg.primitive; break; default: this.error("Unsupported value for 'primitive': '" + cfg.primitive + "' - supported values are 'points', 'lines', 'line-loop', 'line-strip', 'triangles', " + "'triangle-strip' and 'triangle-fan'. Defaulting to 'triangles'."); state.primitive = gl.TRIANGLES; state.primitiveName = cfg.primitive; } if (!cfg.positions) { this.error("Config expected: positions"); return; // TODO: Recover? } if (!cfg.indices) { this.error("Config expected: indices"); return; // TODO: Recover? } var positions; { const positionsDecodeMatrix = cfg.positionsDecodeMatrix; if (positionsDecodeMatrix) ; else { // Uncompressed positions const bounds = geometryCompressionUtils.getPositionsBounds(cfg.positions); const result = geometryCompressionUtils.compressPositions(cfg.positions, bounds.min, bounds.max); positions = result.quantized; state.positionsDecodeMatrix = result.decodeMatrix; state.positionsBuf = new ArrayBuf(gl, gl.ARRAY_BUFFER, positions, positions.length, 3, gl.STATIC_DRAW); memoryStats.positions += state.positionsBuf.numItems; math.positions3ToAABB3(cfg.positions, this._aabb); math.positions3ToAABB3(positions, tempAABB$2, state.positionsDecodeMatrix); math.AABB3ToOBB3(tempAABB$2, this._obb); } } if (cfg.colors) { const colors = cfg.colors.constructor === Float32Array ? cfg.colors : new Float32Array(cfg.colors); state.colorsBuf = new ArrayBuf(gl, gl.ARRAY_BUFFER, colors, colors.length, 4, gl.STATIC_DRAW); memoryStats.colors += state.colorsBuf.numItems; } if (cfg.uv) { const bounds = geometryCompressionUtils.getUVBounds(cfg.uv); const result = geometryCompressionUtils.compressUVs(cfg.uv, bounds.min, bounds.max); const uv = result.quantized; state.uvDecodeMatrix = result.decodeMatrix; state.uvBuf = new ArrayBuf(gl, gl.ARRAY_BUFFER, uv, uv.length, 2, gl.STATIC_DRAW); memoryStats.uvs += state.uvBuf.numItems; } if (cfg.normals) { const normals = geometryCompressionUtils.compressNormals(cfg.normals); let normalized = state.compressGeometry; state.normalsBuf = new ArrayBuf(gl, gl.ARRAY_BUFFER, normals, normals.length, 3, gl.STATIC_DRAW, normalized); memoryStats.normals += state.normalsBuf.numItems; } { const indices = (cfg.indices.constructor === Uint32Array || cfg.indices.constructor === Uint16Array) ? cfg.indices : new Uint32Array(cfg.indices); state.indicesBuf = new ArrayBuf(gl, gl.ELEMENT_ARRAY_BUFFER, indices, indices.length, 1, gl.STATIC_DRAW); memoryStats.indices += state.indicesBuf.numItems; const edgeIndices = buildEdgeIndices(positions, indices, state.positionsDecodeMatrix, this._edgeThreshold); this._edgeIndicesBuf = new ArrayBuf(gl, gl.ELEMENT_ARRAY_BUFFER, edgeIndices, edgeIndices.length, 1, gl.STATIC_DRAW); if (this._state.primitiveName === "triangles") { this._numTriangles = (cfg.indices.length / 3); } } this._buildHash(); memoryStats.meshes++; } _buildHash() { const state = this._state; const hash = ["/g"]; hash.push("/" + state.primitive + ";"); if (state.positionsBuf) { hash.push("p"); } if (state.colorsBuf) { hash.push("c"); } if (state.normalsBuf || state.autoVertexNormals) { hash.push("n"); } if (state.uvBuf) { hash.push("u"); } hash.push("cp"); // Always compressed hash.push(";"); state.hash = hash.join(""); } _getEdgeIndices() { return this._edgeIndicesBuf; } /** * Gets the primitive type. * * Possible types are: 'points', 'lines', 'line-loop', 'line-strip', 'triangles', 'triangle-strip' and 'triangle-fan'. * * @type {String} */ get primitive() { return this._state.primitiveName; } /** * Gets the local-space axis-aligned 3D boundary (AABB). * * The AABB is represented by a six-element Float64Array containing the min/max extents of the axis-aligned volume, ie. ````[xmin, ymin,zmin,xmax,ymax, zmax]````. * * @type {Number[]} */ get aabb() { return this._aabb; } /** * Gets the local-space oriented 3D boundary (OBB). * * The OBB is represented by a 32-element Float64Array containing the eight vertices of the box, where each vertex is a homogeneous coordinate having [x,y,z,w] elements. * * @type {Number[]} */ get obb() { return this._obb; } /** * Approximate number of triangles in this VBOGeometry. * * Will be zero if {@link VBOGeometry#primitive} is not 'triangles', 'triangle-strip' or 'triangle-fan'. * * @type {Number} */ get numTriangles() { return this._numTriangles; } /** @private */ _getState() { return this._state; } /** * Destroys this component. */ destroy() { super.destroy(); const state = this._state; if (state.indicesBuf) { state.indicesBuf.destroy(); } if (state.positionsBuf) { state.positionsBuf.destroy(); } if (state.normalsBuf) { state.normalsBuf.destroy(); } if (state.uvBuf) { state.uvBuf.destroy(); } if (state.colorsBuf) { state.colorsBuf.destroy(); } if (this._edgeIndicesBuf) { this._edgeIndicesBuf.destroy(); } state.destroy(); memoryStats.meshes--; } } /** * @private */ var K3D = {}; K3D.load = function(path, resp) { var request = new XMLHttpRequest(); request.open("GET", path, true); request.responseType = "arraybuffer"; request.onload = function(e){resp(e.target.response);}; request.send(); }; K3D.save = function(buff, path) { var dataURI = "data:application/octet-stream;base64," + btoa(K3D.parse._buffToStr(buff)); window.location.href = dataURI; }; K3D.clone = function(o) { return JSON.parse(JSON.stringify(o)); }; K3D.bin = {}; K3D.bin.f = new Float32Array(1); K3D.bin.fb = new Uint8Array(K3D.bin.f.buffer); K3D.bin.rf = function(buff, off) { var f = K3D.bin.f, fb = K3D.bin.fb; for(var i=0; i<4; i++) fb[i] = buff[off+i]; return f[0]; }; K3D.bin.rsl = function(buff, off) { return buff[off] | buff[off+1]<<8; }; K3D.bin.ril = function(buff, off) { return buff[off] | buff[off+1]<<8 | buff[off+2]<<16 | buff[off+3]<<24; }; K3D.bin.rASCII0 = function(buff, off) { var s = ""; while(buff[off]!=0) s += String.fromCharCode(buff[off++]); return s; }; K3D.bin.wf = function(buff, off, v) { var f=new Float32Array(buff.buffer, off, 1); f[0]=v; }; K3D.bin.wsl = function(buff, off, v) { buff[off]=v; buff[off+1]=v>>8; }; K3D.bin.wil = function(buff, off, v) { buff[off]=v; buff[off+1]=v>>8; buff[off+2]=v>>16; buff[off+3]>>24; }; K3D.parse = {}; K3D.parse._buffToStr = function(buff) { var a = new Uint8Array(buff); var s = ""; for(var i=0; imaxx) maxx = vx; if(vymaxy) maxy = vy; if(vzmaxz) maxz = vz; } return {min:{x:minx, y:miny, z:minz}, max:{x:maxx, y:maxy, z:maxz}}; }; /** * @desc Loads {@link Geometry} from 3DS. * * ## Usage * * In the example below we'll create a {@link Mesh} with {@link PhongMaterial}, {@link Texture} and a {@link ReadableGeometry} loaded from 3DS. * * ````javascript * import {Viewer, Mesh, load3DSGeometry, ReadableGeometry, PhongMaterial, Texture} from "xeokit-sdk.es.js"; * * const viewer = new Viewer({ * canvasId: "myCanvas" * }); * * viewer.scene.camera.eye = [40.04, 23.46, 79.06]; * viewer.scene.camera.look = [-6.48, 13.92, -0.56]; * viewer.scene.camera.up = [-0.04, 0.98, -0.08]; * * load3DSGeometry(viewer.scene, { * src: "models/3ds/lexus.3ds", * compressGeometry: false * * }).then(function (geometryCfg) { * * // Success * * new Mesh(viewer.scene, { * * geometry: new ReadableGeometry(viewer.scene, geometryCfg), * * material: new PhongMaterial(viewer.scene, { * * emissive: [1, 1, 1], * emissiveMap: new Texture({ // .3DS has no normals so relies on emissive illumination * src: "models/3ds/lexus.jpg" * }) * }), * * rotation: [-90, 0, 0] // +Z is up for this particular 3DS * }); * }, function () { * // Error * }); * ```` * * @function load3DSGeometry * @param {Scene} scene Scene we're loading the geometry for. * @param {*} cfg Configs, also added to the result object. * @param {String} [cfg.src] Path to 3DS file. * @returns {Object} Configuration to pass into a {@link Geometry} constructor, containing geometry arrays loaded from the OBJ file. */ function load3DSGeometry(scene, cfg = {}) { return new Promise(function (resolve, reject) { if (!cfg.src) { console.error("load3DSGeometry: Parameter expected: src"); reject(); } var spinner = scene.canvas.spinner; spinner.processes++; utils.loadArraybuffer(cfg.src, function (data) { if (!data.byteLength) { console.error("load3DSGeometry: no data loaded"); spinner.processes--; reject(); } var m = K3D.parse.from3DS(data); // done ! var mesh = m.edit.objects[0].mesh; var positions = mesh.vertices; var uv = mesh.uvt; var indices = mesh.indices; spinner.processes--; resolve(utils.apply(cfg, { primitive: "triangles", positions: positions, normals: null, uv: uv, indices: indices })); }, function (msg) { console.error("load3DSGeometry: " + msg); spinner.processes--; reject(); }); }); } /** * @desc Loads {@link Geometry} from OBJ. * * ## Usage * * In the example below we'll create a {@link Mesh} with {@link MetallicMaterial} and {@link ReadableGeometry} loaded from OBJ. * * ````javascript * import {Viewer, Mesh, loadOBJGeometry, ReadableGeometry, * MetallicMaterial, Texture} from "xeokit-sdk.es.js"; * * const viewer = new Viewer({ * canvasId: "myCanvas" * }); * * viewer.scene.camera.eye = [0.57, 1.37, 1.14]; * viewer.scene.camera.look = [0.04, 0.58, 0.00]; * viewer.scene.camera.up = [-0.22, 0.84, -0.48]; * * loadOBJGeometry(viewer.scene, { * * src: "models/obj/fireHydrant/FireHydrantMesh.obj", * compressGeometry: false * * }).then(function (geometryCfg) { * * // Success * * new Mesh(viewer.scene, { * * geometry: new ReadableGeometry(viewer.scene, geometryCfg), * * material: new MetallicMaterial(viewer.scene, { * * baseColor: [1, 1, 1], * metallic: 1.0, * roughness: 1.0, * * baseColorMap: new Texture(viewer.scene, { * src: "models/obj/fireHydrant/fire_hydrant_Base_Color.png", * encoding: "sRGB" * }), * normalMap: new Texture(viewer.scene, { * src: "models/obj/fireHydrant/fire_hydrant_Normal_OpenGL.png" * }), * roughnessMap: new Texture(viewer.scene, { * src: "models/obj/fireHydrant/fire_hydrant_Roughness.png" * }), * metallicMap: new Texture(viewer.scene, { * src: "models/obj/fireHydrant/fire_hydrant_Metallic.png" * }), * occlusionMap: new Texture(viewer.scene, { * src: "models/obj/fireHydrant/fire_hydrant_Mixed_AO.png" * }), * * specularF0: 0.7 * }) * }); * }, function () { * // Error * }); * ```` * * @function loadOBJGeometry * @param {Scene} scene Scene we're loading the geometry for. * @param {*} [cfg] Configs, also added to the result object. * @param {String} [cfg.src] Path to OBJ file. * @returns {Object} Configuration to pass into a {@link Geometry} constructor, containing geometry arrays loaded from the OBJ file. */ function loadOBJGeometry(scene, cfg = {}) { return new Promise(function (resolve, reject) { if (!cfg.src) { console.error("loadOBJGeometry: Parameter expected: src"); reject(); } var spinner = scene.canvas.spinner; spinner.processes++; utils.loadArraybuffer(cfg.src, function (data) { if (!data.byteLength) { console.error("loadOBJGeometry: no data loaded"); spinner.processes--; reject(); } var m = K3D.parse.fromOBJ(data); // done ! // unwrap simply duplicates some values, so they can be indexed with indices [0,1,2,3 ... ] // In some rendering engines, you can have only one index value for vertices, UVs, normals ..., // so "unwrapping" is a simple solution. var positions = K3D.edit.unwrap(m.i_verts, m.c_verts, 3); var normals = K3D.edit.unwrap(m.i_norms, m.c_norms, 3); var uv = K3D.edit.unwrap(m.i_uvt, m.c_uvt, 2); var indices = new Int32Array(m.i_verts.length); for (var i = 0; i < m.i_verts.length; i++) { indices[i] = i; } spinner.processes--; resolve(utils.apply(cfg, { primitive: "triangles", positions: positions, normals: normals.length > 0 ? normals : null, autoNormals: normals.length === 0, uv: uv, indices: indices })); }, function (msg) { console.error("loadOBJGeometry: " + msg); spinner.processes--; reject(); }); }); } /** * @desc Creates box-shaped {@link Geometry}. * * ## Usage * * In the example below we'll create a {@link Mesh} with a box-shaped {@link ReadableGeometry}. * * [[Run this example](/examples/index.html#geometry_builders_buildBoxGeometry)] * * ````javascript * import {Viewer, Mesh, buildBoxGeometry, ReadableGeometry, PhongMaterial, Texture} from "xeokit-sdk.es.js"; * * const viewer = new Viewer({ * canvasId: "myCanvas" * }); * * viewer.scene.camera.eye = [0, 0, 5]; * viewer.scene.camera.look = [0, 0, 0]; * viewer.scene.camera.up = [0, 1, 0]; * * new Mesh(viewer.scene, { * geometry: new ReadableGeometry(viewer.scene, buildBoxGeometry({ * center: [0,0,0], * xSize: 1, // Half-size on each axis * ySize: 1, * zSize: 1 * }), * material: new PhongMaterial(viewer.scene, { * diffuseMap: new Texture(viewer.scene, { * src: "textures/diffuse/uvGrid2.jpg" * }) * }) * }); * ```` * * @function buildBoxGeometry * @param {*} [cfg] Configs * @param {String} [cfg.id] Optional ID, unique among all components in the parent {@link Scene}, generated automatically when omitted. * @param {Number[]} [cfg.center] 3D point indicating the center position. * @param {Number} [cfg.xSize=1.0] Half-size on the X-axis. * @param {Number} [cfg.ySize=1.0] Half-size on the Y-axis. * @param {Number} [cfg.zSize=1.0] Half-size on the Z-axis. * @returns {Object} Configuration for a {@link Geometry} subtype. */ function buildBoxGeometry(cfg = {}) { let xSize = cfg.xSize || 1; if (xSize < 0) { console.error("negative xSize not allowed - will invert"); xSize *= -1; } let ySize = cfg.ySize || 1; if (ySize < 0) { console.error("negative ySize not allowed - will invert"); ySize *= -1; } let zSize = cfg.zSize || 1; if (zSize < 0) { console.error("negative zSize not allowed - will invert"); zSize *= -1; } const center = cfg.center; const centerX = center ? center[0] : 0; const centerY = center ? center[1] : 0; const centerZ = center ? center[2] : 0; const xmin = -xSize + centerX; const ymin = -ySize + centerY; const zmin = -zSize + centerZ; const xmax = xSize + centerX; const ymax = ySize + centerY; const zmax = zSize + centerZ; return utils.apply(cfg, { // The vertices - eight for our cube, each // one spanning three array elements for X,Y and Z positions: [ // v0-v1-v2-v3 front xmax, ymax, zmax, xmin, ymax, zmax, xmin, ymin, zmax, xmax, ymin, zmax, // v0-v3-v4-v1 right xmax, ymax, zmax, xmax, ymin, zmax, xmax, ymin, zmin, xmax, ymax, zmin, // v0-v1-v6-v1 top xmax, ymax, zmax, xmax, ymax, zmin, xmin, ymax, zmin, xmin, ymax, zmax, // v1-v6-v7-v2 left xmin, ymax, zmax, xmin, ymax, zmin, xmin, ymin, zmin, xmin, ymin, zmax, // v7-v4-v3-v2 bottom xmin, ymin, zmin, xmax, ymin, zmin, xmax, ymin, zmax, xmin, ymin, zmax, // v4-v7-v6-v1 back xmax, ymin, zmin, xmin, ymin, zmin, xmin, ymax, zmin, xmax, ymax, zmin ], // Normal vectors, one for each vertex normals: [ // v0-v1-v2-v3 front 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, // v0-v3-v4-v5 right 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, // v0-v5-v6-v1 top 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, // v1-v6-v7-v2 left -1, 0, 0, -1, 0, 0, -1, 0, 0, -1, 0, 0, // v7-v4-v3-v2 bottom 0, -1, 0, 0, -1, 0, 0, -1, 0, 0, -1, 0, // v4-v7-v6-v5 back 0, 0, -1, 0, 0, -1, 0, 0, -1, 0, 0, -1 ], // UV coords uv: [ // v0-v1-v2-v3 front 1, 0, 0, 0, 0, 1, 1, 1, // v0-v3-v4-v1 right 0, 0, 0, 1, 1, 1, 1, 0, // v0-v1-v6-v1 top 1, 1, 1, 0, 0, 0, 0, 1, // v1-v6-v7-v2 left 1, 0, 0, 0, 0, 1, 1, 1, // v7-v4-v3-v2 bottom 0, 1, 1, 1, 1, 0, 0, 0, // v4-v7-v6-v1 back 0, 1, 1, 1, 1, 0, 0, 0 ], // Indices - these organise the // positions and uv texture coordinates // into geometric primitives in accordance // with the "primitive" parameter, // in this case a set of three indices // for each triangle. // // Note that each triangle is specified // in counter-clockwise winding order. // // You can specify them in clockwise // order if you configure the Modes // node's frontFace flag as "cw", instead of // the default "ccw". indices: [ 0, 1, 2, 0, 2, 3, // front 4, 5, 6, 4, 6, 7, // right 8, 9, 10, 8, 10, 11, // top 12, 13, 14, 12, 14, 15, // left 16, 17, 18, 16, 18, 19, // bottom 20, 21, 22, 20, 22, 23 ] }); } /** * @desc Creates a box-shaped lines {@link Geometry}. * * ## Usage * * In the example below we'll create a {@link Mesh} with a box-shaped {@link ReadableGeometry} that has lines primitives. * * [[Run this example](/examples/index.html#geometry_builders_buildBoxLinesGeometry)] * * ````javascript * import {Viewer, Mesh, buildBoxLinesGeometry, ReadableGeometry, PhongMaterial} from "xeokit-sdk.es.js"; * * const viewer = new Viewer({ * canvasId: "myCanvas" * }); * * viewer.scene.camera.eye = [0, 0, 5]; * viewer.scene.camera.look = [0, 0, 0]; * viewer.scene.camera.up = [0, 1, 0]; * * new Mesh(viewer.scene, { * geometry: new ReadableGeometry(viewer.scene, buildBoxLinesGeometry({ * center: [0,0,0], * xSize: 1, // Half-size on each axis * ySize: 1, * zSize: 1 * })), * material: new PhongMaterial(viewer.scene, { * emissive: [0,1,0] * }) * }); * ```` * * @function buildBoxLinesGeometry * @param {*} [cfg] Configs * @param {String} [cfg.id] Optional ID, unique among all components in the parent {@link Scene}, generated automatically when omitted. * @param {Number[]} [cfg.center] 3D point indicating the center position. * @param {Number} [cfg.xSize=1.0] Half-size on the X-axis. * @param {Number} [cfg.ySize=1.0] Half-size on the Y-axis. * @param {Number} [cfg.zSize=1.0] Half-size on the Z-axis. * @returns {Object} Configuration for a {@link Geometry} subtype. */ function buildBoxLinesGeometry(cfg = {}) { let xSize = cfg.xSize || 1; if (xSize < 0) { console.error("negative xSize not allowed - will invert"); xSize *= -1; } let ySize = cfg.ySize || 1; if (ySize < 0) { console.error("negative ySize not allowed - will invert"); ySize *= -1; } let zSize = cfg.zSize || 1; if (zSize < 0) { console.error("negative zSize not allowed - will invert"); zSize *= -1; } const center = cfg.center; const centerX = center ? center[0] : 0; const centerY = center ? center[1] : 0; const centerZ = center ? center[2] : 0; const xmin = -xSize + centerX; const ymin = -ySize + centerY; const zmin = -zSize + centerZ; const xmax = xSize + centerX; const ymax = ySize + centerY; const zmax = zSize + centerZ; return utils.apply(cfg, { primitive: "lines", positions: [ xmin, ymin, zmin, xmin, ymin, zmax, xmin, ymax, zmin, xmin, ymax, zmax, xmax, ymin, zmin, xmax, ymin, zmax, xmax, ymax, zmin, xmax, ymax, zmax ], indices: [ 0, 1, 1, 3, 3, 2, 2, 0, 4, 5, 5, 7, 7, 6, 6, 4, 0, 4, 1, 5, 2, 6, 3, 7 ] }); } /** * @desc Creates a box-shaped lines {@link Geometry} from AABB. * * ## Usage * * In the example below we'll create a {@link Mesh} with a box-shaped {@link ReadableGeometry} that has lines primitives. * This box will be created from AABB of a model. * * [[Run this example](/examples/index.html#geometry_builders_buildBoxLinesGeometryFromAABB)] * * ````javascript * import {Viewer, Mesh, Node, buildBoxGeometry, buildBoxLinesGeometryFromAABB, ReadableGeometry, PhongMaterial} from "../../dist/xeokit-sdk.min.es.js"; * * const viewer = new Viewer({ * canvasId: "myCanvas" * }); * * viewer.scene.camera.eye = [-21.80, 4.01, 6.56]; * viewer.scene.camera.look = [0, -5.75, 0]; * viewer.scene.camera.up = [0.37, 0.91, -0.11]; * * const boxGeometry = new ReadableGeometry(viewer.scene, buildBoxGeometry({ * xSize: 1, * ySize: 1, * zSize: 1 * })); * * new Node(viewer.scene, { * id: "table", * isModel: true, // <--------------------- Node represents a model * rotation: [0, 50, 0], * position: [0, 0, 0], * scale: [1, 1, 1], * * children: [ * * new Mesh(viewer.scene, { // Red table leg * id: "redLeg", * isObject: true, // <---------- Node represents an object * position: [-4, -6, -4], * scale: [1, 3, 1], * rotation: [0, 0, 0], * geometry: boxGeometry, * material: new PhongMaterial(viewer.scene, { * diffuse: [1, 0.3, 0.3] * }) * }), * * new Mesh(viewer.scene, { // Green table leg * id: "greenLeg", * isObject: true, // <---------- Node represents an object * position: [4, -6, -4], * scale: [1, 3, 1], * rotation: [0, 0, 0], * geometry: boxGeometry, * material: new PhongMaterial(viewer.scene, { * diffuse: [0.3, 1.0, 0.3] * }) * }), * * new Mesh(viewer.scene, {// Blue table leg * id: "blueLeg", * isObject: true, // <---------- Node represents an object * position: [4, -6, 4], * scale: [1, 3, 1], * rotation: [0, 0, 0], * geometry: boxGeometry, * material: new PhongMaterial(viewer.scene, { * diffuse: [0.3, 0.3, 1.0] * }) * }), * * new Mesh(viewer.scene, { // Yellow table leg * id: "yellowLeg", * isObject: true, // <---------- Node represents an object * position: [-4, -6, 4], * scale: [1, 3, 1], * rotation: [0, 0, 0], * geometry: boxGeometry, * material: new PhongMaterial(viewer.scene, { * diffuse: [1.0, 1.0, 0.0] * }) * }), * * new Mesh(viewer.scene, { // Purple table top * id: "tableTop", * isObject: true, // <---------- Node represents an object * position: [0, -3, 0], * scale: [6, 0.5, 6], * rotation: [0, 0, 0], * geometry: boxGeometry, * material: new PhongMaterial(viewer.scene, { * diffuse: [1.0, 0.3, 1.0] * }) * }) * ] * }); * * let aabb = viewer.scene.aabb; * console.log(aabb); * * new Mesh(viewer.scene, { * geometry: new ReadableGeometry(viewer.scene, buildBoxLinesGeometryFromAABB({ * id: "aabb", * aabb: aabb, * })), * material: new PhongMaterial(viewer.scene, { * emissive: [0, 1,] * }) * }); * ```` * * @function buildBoxLinesGeometryFromAABB * @param {*} [cfg] Configs * @param {String} [cfg.id] Optional ID, unique among all components in the parent {@link Scene}, generated automatically when omitted. * @param {Number[]} [cfg.aabb] AABB for which box will be created. * @returns {Object} Configuration for a {@link Geometry} subtype. */ function buildBoxLinesGeometryFromAABB(cfg = {}){ return buildBoxLinesGeometry({ id: cfg.id, center: [ (cfg.aabb[0] + cfg.aabb[3]) / 2.0, (cfg.aabb[1] + cfg.aabb[4]) / 2.0, (cfg.aabb[2] + cfg.aabb[5]) / 2.0, ], xSize: (Math.abs(cfg.aabb[3] - cfg.aabb[0])) / 2.0, ySize: (Math.abs(cfg.aabb[4] - cfg.aabb[1])) / 2.0, zSize: (Math.abs(cfg.aabb[5] - cfg.aabb[2])) / 2.0, }); } /** * @desc Creates a cylinder-shaped {@link Geometry}. * * ## Usage * * Creating a {@link Mesh} with a cylinder-shaped {@link ReadableGeometry} : * * [[Run this example](/examples/index.html#geometry_builders_buildCylinderGeometry)] * * ````javascript * * import {Viewer, Mesh, buildCylinderGeometry, ReadableGeometry, PhongMaterial, Texture} from "xeokit-sdk.es.js"; * * const viewer = new Viewer({ * canvasId: "myCanvas" * }); * * viewer.camera.eye = [0, 0, 5]; * viewer.camera.look = [0, 0, 0]; * viewer.camera.up = [0, 1, 0]; * * new Mesh(viewer.scene, { * geometry: new ReadableGeometry(viewer.scene, buildCylinderGeometry({ * center: [0,0,0], * radiusTop: 2.0, * radiusBottom: 2.0, * height: 5.0, * radialSegments: 20, * heightSegments: 1, * openEnded: false * }), * material: new PhongMaterial(viewer.scene, { * diffuseMap: new Texture(viewer.scene, { * src: "textures/diffuse/uvGrid2.jpg" * }) * }) * }); * ```` * * @function buildCylinderGeometry * @param {*} [cfg] Configs * @param {String} [cfg.id] Optional ID for the {@link Geometry}, unique among all components in the parent {@link Scene}, generated automatically when omitted. * @param {Number[]} [cfg.center] 3D point indicating the center position. * @param {Number} [cfg.radiusTop=1] Radius of top. * @param {Number} [cfg.radiusBottom=1] Radius of bottom. * @param {Number} [cfg.height=1] Height. * @param {Number} [cfg.radialSegments=60] Number of horizontal segments. * @param {Number} [cfg.heightSegments=1] Number of vertical segments. * @param {Boolean} [cfg.openEnded=false] Whether or not the cylinder has solid caps on the ends. * @returns {Object} Configuration for a {@link Geometry} subtype. */ function buildCylinderGeometry(cfg = {}) { let radiusTop = cfg.radiusTop || 1; if (radiusTop < 0) { console.error("negative radiusTop not allowed - will invert"); radiusTop *= -1; } let radiusBottom = cfg.radiusBottom || 1; if (radiusBottom < 0) { console.error("negative radiusBottom not allowed - will invert"); radiusBottom *= -1; } let height = cfg.height || 1; if (height < 0) { console.error("negative height not allowed - will invert"); height *= -1; } let radialSegments = cfg.radialSegments || 32; if (radialSegments < 0) { console.error("negative radialSegments not allowed - will invert"); radialSegments *= -1; } if (radialSegments < 3) { radialSegments = 3; } let heightSegments = cfg.heightSegments || 1; if (heightSegments < 0) { console.error("negative heightSegments not allowed - will invert"); heightSegments *= -1; } if (heightSegments < 1) { heightSegments = 1; } const openEnded = !!cfg.openEnded; let center = cfg.center; const centerX = center ? center[0] : 0; const centerY = center ? center[1] : 0; const centerZ = center ? center[2] : 0; const heightHalf = height / 2; const heightLength = height / heightSegments; const radialAngle = (2.0 * Math.PI / radialSegments); const radialLength = 1.0 / radialSegments; //var nextRadius = this._radiusBottom; const radiusChange = (radiusTop - radiusBottom) / heightSegments; const positions = []; const normals = []; const uvs = []; const indices = []; let h; let i; let x; let z; let currentRadius; let currentHeight; let first; let second; let startIndex; let tu; let tv; // create vertices const normalY = (90.0 - (Math.atan(height / (radiusBottom - radiusTop))) * 180 / Math.PI) / 90.0; for (h = 0; h <= heightSegments; h++) { currentRadius = radiusTop - h * radiusChange; currentHeight = heightHalf - h * heightLength; for (i = 0; i <= radialSegments; i++) { x = Math.sin(i * radialAngle); z = Math.cos(i * radialAngle); normals.push(currentRadius * x); normals.push(normalY); //todo normals.push(currentRadius * z); uvs.push((i * radialLength)); uvs.push(h * 1 / heightSegments); positions.push((currentRadius * x) + centerX); positions.push((currentHeight) + centerY); positions.push((currentRadius * z) + centerZ); } } // create faces for (h = 0; h < heightSegments; h++) { for (i = 0; i <= radialSegments; i++) { first = h * (radialSegments + 1) + i; second = first + radialSegments; indices.push(first); indices.push(second); indices.push(second + 1); indices.push(first); indices.push(second + 1); indices.push(first + 1); } } // create top cap if (!openEnded && radiusTop > 0) { startIndex = (positions.length / 3); // top center normals.push(0.0); normals.push(1.0); normals.push(0.0); uvs.push(0.5); uvs.push(0.5); positions.push(0 + centerX); positions.push(heightHalf + centerY); positions.push(0 + centerZ); // top triangle fan for (i = 0; i <= radialSegments; i++) { x = Math.sin(i * radialAngle); z = Math.cos(i * radialAngle); tu = (0.5 * Math.sin(i * radialAngle)) + 0.5; tv = (0.5 * Math.cos(i * radialAngle)) + 0.5; normals.push(radiusTop * x); normals.push(1.0); normals.push(radiusTop * z); uvs.push(tu); uvs.push(tv); positions.push((radiusTop * x) + centerX); positions.push((heightHalf) + centerY); positions.push((radiusTop * z) + centerZ); } for (i = 0; i < radialSegments; i++) { center = startIndex; first = startIndex + 1 + i; indices.push(first); indices.push(first + 1); indices.push(center); } } // create bottom cap if (!openEnded && radiusBottom > 0) { startIndex = (positions.length / 3); // top center normals.push(0.0); normals.push(-1.0); normals.push(0.0); uvs.push(0.5); uvs.push(0.5); positions.push(0 + centerX); positions.push(0 - heightHalf + centerY); positions.push(0 + centerZ); // top triangle fan for (i = 0; i <= radialSegments; i++) { x = Math.sin(i * radialAngle); z = Math.cos(i * radialAngle); tu = (0.5 * Math.sin(i * radialAngle)) + 0.5; tv = (0.5 * Math.cos(i * radialAngle)) + 0.5; normals.push(radiusBottom * x); normals.push(-1.0); normals.push(radiusBottom * z); uvs.push(tu); uvs.push(tv); positions.push((radiusBottom * x) + centerX); positions.push((0 - heightHalf) + centerY); positions.push((radiusBottom * z) + centerZ); } for (i = 0; i < radialSegments; i++) { center = startIndex; first = startIndex + 1 + i; indices.push(center); indices.push(first + 1); indices.push(first); } } return utils.apply(cfg, { positions: positions, normals: normals, uv: uvs, indices: indices }); } /** * @desc Creates a grid-shaped {@link Geometry}. * * ## Usage * * Creating a {@link Mesh} with a GridGeometry and a {@link PhongMaterial}: * * [[Run this example](/examples/index.html#geometry_builders_buildGridGeometry)] * * ````javascript * import {Viewer, Mesh, buildGridGeometry, VBOGeometry, PhongMaterial, Texture} from "xeokit-sdk.es.js"; * * const viewer = new Viewer({ * canvasId: "myCanvas" * }); * * viewer.camera.eye = [0, 0, 5]; * viewer.camera.look = [0, 0, 0]; * viewer.camera.up = [0, 1, 0]; * * new Mesh(viewer.scene, { * geometry: new VBOGeometry(viewer.scene, buildGridGeometry({ * size: 1000, * divisions: 500 * })), * material: new PhongMaterial(viewer.scene, { * color: [0.0, 0.0, 0.0], * emissive: [0.4, 0.4, 0.4] * }), * position: [0, -1.6, 0] * }); * ```` * * @function buildGridGeometry * @param {*} [cfg] Configs * @param {String} [cfg.id] Optional ID for the {@link Geometry}, unique among all components in the parent {@link Scene}, generated automatically when omitted. * @param {Number} [cfg.size=1] Dimension on the X and Z-axis. * @param {Number} [cfg.divisions=1] Number of divisions on X and Z axis.. * @returns {Object} Configuration for a {@link Geometry} subtype. */ function buildGridGeometry(cfg = {}) { let size = cfg.size || 1; if (size < 0) { console.error("negative size not allowed - will invert"); size *= -1; } let divisions = cfg.divisions || 1; if (divisions < 0) { console.error("negative divisions not allowed - will invert"); divisions *= -1; } if (divisions < 1) { divisions = 1; } size = size || 10; divisions = divisions || 10; const step = size / divisions; const halfSize = size / 2; const positions = []; const indices = []; let l = 0; for (let i = 0, k = -halfSize; i <= divisions; i++, k += step) { positions.push(-halfSize); positions.push(0); positions.push(k); positions.push(halfSize); positions.push(0); positions.push(k); positions.push(k); positions.push(0); positions.push(-halfSize); positions.push(k); positions.push(0); positions.push(halfSize); indices.push(l++); indices.push(l++); indices.push(l++); indices.push(l++); } return utils.apply(cfg, { primitive: "lines", positions: positions, indices: indices }); } /** * @desc Creates a plane-shaped {@link Geometry}. * * ## Usage * * Creating a {@link Mesh} with a PlaneGeometry and a {@link PhongMaterial} with diffuse {@link Texture}: * * [[Run this example](https://xeokit.github.io/xeokit-sdk/examples/scenegraph/#buildPlaneGeometry)] * * ````javascript * import {Viewer, Mesh, buildPlaneGeometry, ReadableGeometry, PhongMaterial, Texture} from "xeokit-sdk.es.js"; * * const viewer = new Viewer({ * canvasId: "myCanvas" * }); * * viewer.camera.eye = [0, 0, 5]; * viewer.camera.look = [0, 0, 0]; * viewer.camera.up = [0, 1, 0]; * new Mesh(viewer.scene, { * geometry: new ReadableGeometry(viewer.scene, buildPlaneGeometry({ * center: [0,0,0], * xSize: 2, * zSize: 2, * xSegments: 10, * zSegments: 10 * }), * material: new PhongMaterial(viewer.scene, { * diffuseMap: new Texture(viewer.scene, { * src: "textures/diffuse/uvGrid2.jpg" * }) * }) * }); * ```` * * @function buildPlaneGeometry * @param {*} [cfg] Configs * @param {Number[]} [cfg.center] 3D point indicating the center position. * @param {String} [cfg.id] Optional ID for the {@link Geometry}, unique among all components in the parent {@link Scene}, generated automatically when omitted. * @param {Number} [cfg.xSize=1] Dimension on the X-axis. * @param {Number} [cfg.zSize=1] Dimension on the Z-axis. * @param {Number} [cfg.xSegments=1] Number of segments on the X-axis. * @param {Number} [cfg.zSegments=1] Number of segments on the Z-axis. * @returns {Object} Configuration for a {@link Geometry} subtype. */ function buildPlaneGeometry(cfg = {}) { let xSize = cfg.xSize || 1; if (xSize < 0) { console.error("negative xSize not allowed - will invert"); xSize *= -1; } let zSize = cfg.zSize || 1; if (zSize < 0) { console.error("negative zSize not allowed - will invert"); zSize *= -1; } let xSegments = cfg.xSegments || 1; if (xSegments < 0) { console.error("negative xSegments not allowed - will invert"); xSegments *= -1; } if (xSegments < 1) { xSegments = 1; } let zSegments = cfg.xSegments || 1; if (zSegments < 0) { console.error("negative zSegments not allowed - will invert"); zSegments *= -1; } if (zSegments < 1) { zSegments = 1; } const center = cfg.center; const centerX = center ? center[0] : 0; const centerY = center ? center[1] : 0; const centerZ = center ? center[2] : 0; const halfWidth = xSize / 2; const halfHeight = zSize / 2; const planeX = Math.floor(xSegments) || 1; const planeZ = Math.floor(zSegments) || 1; const planeX1 = planeX + 1; const planeZ1 = planeZ + 1; const segmentWidth = xSize / planeX; const segmentHeight = zSize / planeZ; const positions = new Float32Array(planeX1 * planeZ1 * 3); const normals = new Float32Array(planeX1 * planeZ1 * 3); const uvs = new Float32Array(planeX1 * planeZ1 * 2); let offset = 0; let offset2 = 0; let iz; let ix; let x; let a; let b; let c; let d; for (iz = 0; iz < planeZ1; iz++) { const z = iz * segmentHeight - halfHeight; for (ix = 0; ix < planeX1; ix++) { x = ix * segmentWidth - halfWidth; positions[offset] = x + centerX; positions[offset + 1] = centerY; positions[offset + 2] = -z + centerZ; normals[offset + 1] = 1; uvs[offset2] = (ix) / planeX; uvs[offset2 + 1] = ((planeZ - iz) / planeZ); offset += 3; offset2 += 2; } } offset = 0; const indices = new ((positions.length / 3) > 65535 ? Uint32Array : Uint16Array)(planeX * planeZ * 6); for (iz = 0; iz < planeZ; iz++) { for (ix = 0; ix < planeX; ix++) { a = ix + planeX1 * iz; b = ix + planeX1 * (iz + 1); c = (ix + 1) + planeX1 * (iz + 1); d = (ix + 1) + planeX1 * iz; indices[offset] = d; indices[offset + 1] = b; indices[offset + 2] = a; indices[offset + 3] = d; indices[offset + 4] = c; indices[offset + 5] = b; offset += 6; } } return utils.apply(cfg, { positions: positions, normals: normals, uv: uvs, indices: indices }); } /** * @desc Creates a sphere-shaped {@link Geometry}. * * ## Usage * * Creating a {@link Mesh} with a sphere-shaped {@link ReadableGeometry} : * * [[Run this example](https://xeokit.github.io/xeokit-sdk/examples/scenegraph/#buildSphereGeometry)] * * ````javascript * import {Viewer, Mesh, buildSphereGeometry, ReadableGeometry, PhongMaterial, Texture} from "xeokit-sdk.es.js"; * * const viewer = new Viewer({ * canvasId: "myCanvas" * }); * * viewer.camera.eye = [0, 0, 5]; * viewer.camera.look = [0, 0, 0]; * viewer.camera.up = [0, 1, 0]; * * new Mesh(viewer.scene, { * geometry: new ReadableGeometry(viewer.scene, buildSphereGeometry({ * center: [0,0,0], * radius: 1.5, * heightSegments: 60, * widthSegments: 60 * }), * material: new PhongMaterial(viewer.scene, { * diffuseMap: new Texture(viewer.scene, { * src: "textures/diffuse/uvGrid2.jpg" * }) * }) * }); * ```` * * @function buildSphereGeometry * @param {*} [cfg] Configs * @param {String} [cfg.id] Optional ID for the {@link Geometry}, unique among all components in the parent {@link Scene}, generated automatically when omitted. * @param {Number[]} [cfg.center] 3D point indicating the center position. * @param {Number} [cfg.radius=1] Radius. * @param {Number} [cfg.heightSegments=24] Number of latitudinal bands. * @param {Number} [cfg.widthSegments=18] Number of longitudinal bands. * @returns {Object} Configuration for a {@link Geometry} subtype. */ function buildSphereGeometry(cfg = {}) { const lod = cfg.lod || 1; const centerX = cfg.center ? cfg.center[0] : 0; const centerY = cfg.center ? cfg.center[1] : 0; const centerZ = cfg.center ? cfg.center[2] : 0; let radius = cfg.radius || 1; if (radius < 0) { console.error("negative radius not allowed - will invert"); radius *= -1; } let heightSegments = cfg.heightSegments || 18; if (heightSegments < 0) { console.error("negative heightSegments not allowed - will invert"); heightSegments *= -1; } heightSegments = Math.floor(lod * heightSegments); if (heightSegments < 18) { heightSegments = 18; } let widthSegments = cfg.widthSegments || 18; if (widthSegments < 0) { console.error("negative widthSegments not allowed - will invert"); widthSegments *= -1; } widthSegments = Math.floor(lod * widthSegments); if (widthSegments < 18) { widthSegments = 18; } const positions = []; const normals = []; const uvs = []; const indices = []; let i; let j; let theta; let sinTheta; let cosTheta; let phi; let sinPhi; let cosPhi; let x; let y; let z; let u; let v; let first; let second; for (i = 0; i <= heightSegments; i++) { theta = i * Math.PI / heightSegments; sinTheta = Math.sin(theta); cosTheta = Math.cos(theta); for (j = 0; j <= widthSegments; j++) { phi = j * 2 * Math.PI / widthSegments; sinPhi = Math.sin(phi); cosPhi = Math.cos(phi); x = cosPhi * sinTheta; y = cosTheta; z = sinPhi * sinTheta; u = 1.0 - j / widthSegments; v = i / heightSegments; normals.push(x); normals.push(y); normals.push(z); uvs.push(u); uvs.push(v); positions.push(centerX + radius * x); positions.push(centerY + radius * y); positions.push(centerZ + radius * z); } } for (i = 0; i < heightSegments; i++) { for (j = 0; j < widthSegments; j++) { first = (i * (widthSegments + 1)) + j; second = first + widthSegments + 1; indices.push(first + 1); indices.push(second + 1); indices.push(second); indices.push(first + 1); indices.push(second); indices.push(first); } } return utils.apply(cfg, { positions: positions, normals: normals, uv: uvs, indices: indices }); } /** * @desc Creates a torus-shaped {@link Geometry}. * * ## Usage * Creating a {@link Mesh} with a torus-shaped {@link ReadableGeometry} : * * [[Run this example](/examples/index.html#geometry_builders_buildTorusGeometry)] * * ````javascript * import {Viewer, Mesh, buildTorusGeometry, ReadableGeometry, PhongMaterial, Texture} from "xeokit-sdk.es.js"; * * const viewer = new Viewer({ * canvasId: "myCanvas" * }); * * viewer.camera.eye = [0, 0, 5]; * viewer.camera.look = [0, 0, 0]; * viewer.camera.up = [0, 1, 0]; * * new Mesh(viewer.scene, { * geometry: new ReadableGeometry(viewer.scene, buildTorusGeometry({ * center: [0,0,0], * radius: 1.0, * tube: 0.5, * radialSegments: 32, * tubeSegments: 24, * arc: Math.PI * 2.0 * }), * material: new PhongMaterial(viewer.scene, { * diffuseMap: new Texture(viewer.scene, { * src: "textures/diffuse/uvGrid2.jpg" * }) * }) * }); * ```` * * @function buildTorusGeometry * @param {*} [cfg] Configs * @param {String} [cfg.id] Optional ID for the {@link Geometry}, unique among all components in the parent {@link Scene}, generated automatically when omitted. * @param {Number[]} [cfg.center] 3D point indicating the center position. * @param {Number} [cfg.radius=1] The overall radius. * @param {Number} [cfg.tube=0.3] The tube radius. * @param {Number} [cfg.radialSegments=32] The number of radial segments. * @param {Number} [cfg.tubeSegments=24] The number of tubular segments. * @param {Number} [cfg.arc=Math.PI*0.5] The length of the arc in radians, where Math.PI*2 is a closed torus. * @returns {Object} Configuration for a {@link Geometry} subtype. */ function buildTorusGeometry(cfg = {}) { let radius = cfg.radius || 1; if (radius < 0) { console.error("negative radius not allowed - will invert"); radius *= -1; } radius *= 0.5; let tube = cfg.tube || 0.3; if (tube < 0) { console.error("negative tube not allowed - will invert"); tube *= -1; } let radialSegments = cfg.radialSegments || 32; if (radialSegments < 0) { console.error("negative radialSegments not allowed - will invert"); radialSegments *= -1; } if (radialSegments < 4) { radialSegments = 4; } let tubeSegments = cfg.tubeSegments || 24; if (tubeSegments < 0) { console.error("negative tubeSegments not allowed - will invert"); tubeSegments *= -1; } if (tubeSegments < 4) { tubeSegments = 4; } let arc = cfg.arc || Math.PI * 2; if (arc < 0) { console.warn("negative arc not allowed - will invert"); arc *= -1; } if (arc > 360) { arc = 360; } const center = cfg.center; let centerX = center ? center[0] : 0; let centerY = center ? center[1] : 0; const centerZ = center ? center[2] : 0; const positions = []; const normals = []; const uvs = []; const indices = []; let u; let v; let x; let y; let z; let vec; let i; let j; for (j = 0; j <= tubeSegments; j++) { for (i = 0; i <= radialSegments; i++) { u = i / radialSegments * arc; v = 0.785398 + (j / tubeSegments * Math.PI * 2); centerX = radius * Math.cos(u); centerY = radius * Math.sin(u); x = (radius + tube * Math.cos(v)) * Math.cos(u); y = (radius + tube * Math.cos(v)) * Math.sin(u); z = tube * Math.sin(v); positions.push(x + centerX); positions.push(y + centerY); positions.push(z + centerZ); uvs.push(1 - (i / radialSegments)); uvs.push((j / tubeSegments)); vec = math.normalizeVec3(math.subVec3([x, y, z], [centerX, centerY, centerZ], []), []); normals.push(vec[0]); normals.push(vec[1]); normals.push(vec[2]); } } let a; let b; let c; let d; for (j = 1; j <= tubeSegments; j++) { for (i = 1; i <= radialSegments; i++) { a = (radialSegments + 1) * j + i - 1; b = (radialSegments + 1) * (j - 1) + i - 1; c = (radialSegments + 1) * (j - 1) + i; d = (radialSegments + 1) * j + i; indices.push(a); indices.push(b); indices.push(c); indices.push(c); indices.push(d); indices.push(a); } } return utils.apply(cfg, { positions: positions, normals: normals, uv: uvs, indices: indices }); } const letters = { ' ': {width: 16, points: []}, '!': { width: 10, points: [ [5, 21], [5, 7], [-1, -1], [5, 2], [4, 1], [5, 0], [6, 1], [5, 2] ] }, '"': { width: 16, points: [ [4, 21], [4, 14], [-1, -1], [12, 21], [12, 14] ] }, '#': { width: 21, points: [ [11, 25], [4, -7], [-1, -1], [17, 25], [10, -7], [-1, -1], [4, 12], [18, 12], [-1, -1], [3, 6], [17, 6] ] }, '$': { width: 20, points: [ [8, 25], [8, -4], [-1, -1], [12, 25], [12, -4], [-1, -1], [17, 18], [15, 20], [12, 21], [8, 21], [5, 20], [3, 18], [3, 16], [4, 14], [5, 13], [7, 12], [13, 10], [15, 9], [16, 8], [17, 6], [17, 3], [15, 1], [12, 0], [8, 0], [5, 1], [3, 3] ] }, '%': { width: 24, points: [ [21, 21], [3, 0], [-1, -1], [8, 21], [10, 19], [10, 17], [9, 15], [7, 14], [5, 14], [3, 16], [3, 18], [4, 20], [6, 21], [8, 21], [10, 20], [13, 19], [16, 19], [19, 20], [21, 21], [-1, -1], [17, 7], [15, 6], [14, 4], [14, 2], [16, 0], [18, 0], [20, 1], [21, 3], [21, 5], [19, 7], [17, 7] ] }, '&': { width: 26, points: [ [23, 12], [23, 13], [22, 14], [21, 14], [20, 13], [19, 11], [17, 6], [15, 3], [13, 1], [11, 0], [7, 0], [5, 1], [4, 2], [3, 4], [3, 6], [4, 8], [5, 9], [12, 13], [13, 14], [14, 16], [14, 18], [13, 20], [11, 21], [9, 20], [8, 18], [8, 16], [9, 13], [11, 10], [16, 3], [18, 1], [20, 0], [22, 0], [23, 1], [23, 2] ] }, '\'': { width: 10, points: [ [5, 19], [4, 20], [5, 21], [6, 20], [6, 18], [5, 16], [4, 15] ] }, '(': { width: 14, points: [ [11, 25], [9, 23], [7, 20], [5, 16], [4, 11], [4, 7], [5, 2], [7, -2], [9, -5], [11, -7] ] }, ')': { width: 14, points: [ [3, 25], [5, 23], [7, 20], [9, 16], [10, 11], [10, 7], [9, 2], [7, -2], [5, -5], [3, -7] ] }, '*': { width: 16, points: [ [8, 21], [8, 9], [-1, -1], [3, 18], [13, 12], [-1, -1], [13, 18], [3, 12] ] }, '+': { width: 26, points: [ [13, 18], [13, 0], [-1, -1], [4, 9], [22, 9] ] }, ',': { width: 10, points: [ [6, 1], [5, 0], [4, 1], [5, 2], [6, 1], [6, -1], [5, -3], [4, -4] ] }, '-': { width: 26, points: [ [4, 9], [22, 9] ] }, '.': { width: 10, points: [ [5, 2], [4, 1], [5, 0], [6, 1], [5, 2] ] }, '/': { width: 22, points: [ [20, 25], [2, -7] ] }, '0': { width: 20, points: [ [9, 21], [6, 20], [4, 17], [3, 12], [3, 9], [4, 4], [6, 1], [9, 0], [11, 0], [14, 1], [16, 4], [17, 9], [17, 12], [16, 17], [14, 20], [11, 21], [9, 21] ] }, '1': { width: 20, points: [ [6, 17], [8, 18], [11, 21], [11, 0] ] }, '2': { width: 20, points: [ [4, 16], [4, 17], [5, 19], [6, 20], [8, 21], [12, 21], [14, 20], [15, 19], [16, 17], [16, 15], [15, 13], [13, 10], [3, 0], [17, 0] ] }, '3': { width: 20, points: [ [5, 21], [16, 21], [10, 13], [13, 13], [15, 12], [16, 11], [17, 8], [17, 6], [16, 3], [14, 1], [11, 0], [8, 0], [5, 1], [4, 2], [3, 4] ] }, '4': { width: 20, points: [ [13, 21], [3, 7], [18, 7], [-1, -1], [13, 21], [13, 0] ] }, '5': { width: 20, points: [ [15, 21], [5, 21], [4, 12], [5, 13], [8, 14], [11, 14], [14, 13], [16, 11], [17, 8], [17, 6], [16, 3], [14, 1], [11, 0], [8, 0], [5, 1], [4, 2], [3, 4] ] }, '6': { width: 20, points: [ [16, 18], [15, 20], [12, 21], [10, 21], [7, 20], [5, 17], [4, 12], [4, 7], [5, 3], [7, 1], [10, 0], [11, 0], [14, 1], [16, 3], [17, 6], [17, 7], [16, 10], [14, 12], [11, 13], [10, 13], [7, 12], [5, 10], [4, 7] ] }, '7': { width: 20, points: [ [17, 21], [7, 0], [-1, -1], [3, 21], [17, 21] ] }, '8': { width: 20, points: [ [8, 21], [5, 20], [4, 18], [4, 16], [5, 14], [7, 13], [11, 12], [14, 11], [16, 9], [17, 7], [17, 4], [16, 2], [15, 1], [12, 0], [8, 0], [5, 1], [4, 2], [3, 4], [3, 7], [4, 9], [6, 11], [9, 12], [13, 13], [15, 14], [16, 16], [16, 18], [15, 20], [12, 21], [8, 21] ] }, '9': { width: 20, points: [ [16, 14], [15, 11], [13, 9], [10, 8], [9, 8], [6, 9], [4, 11], [3, 14], [3, 15], [4, 18], [6, 20], [9, 21], [10, 21], [13, 20], [15, 18], [16, 14], [16, 9], [15, 4], [13, 1], [10, 0], [8, 0], [5, 1], [4, 3] ] }, ':': { width: 10, points: [ [5, 14], [4, 13], [5, 12], [6, 13], [5, 14], [-1, -1], [5, 2], [4, 1], [5, 0], [6, 1], [5, 2] ] }, ';': { width: 10, points: [ [5, 14], [4, 13], [5, 12], [6, 13], [5, 14], [-1, -1], [6, 1], [5, 0], [4, 1], [5, 2], [6, 1], [6, -1], [5, -3], [4, -4] ] }, '<': { width: 24, points: [ [20, 18], [4, 9], [20, 0] ] }, '=': { width: 26, points: [ [4, 12], [22, 12], [-1, -1], [4, 6], [22, 6] ] }, '>': { width: 24, points: [ [4, 18], [20, 9], [4, 0] ] }, '?': { width: 18, points: [ [3, 16], [3, 17], [4, 19], [5, 20], [7, 21], [11, 21], [13, 20], [14, 19], [15, 17], [15, 15], [14, 13], [13, 12], [9, 10], [9, 7], [-1, -1], [9, 2], [8, 1], [9, 0], [10, 1], [9, 2] ] }, '@': { width: 27, points: [ [18, 13], [17, 15], [15, 16], [12, 16], [10, 15], [9, 14], [8, 11], [8, 8], [9, 6], [11, 5], [14, 5], [16, 6], [17, 8], [-1, -1], [12, 16], [10, 14], [9, 11], [9, 8], [10, 6], [11, 5], [-1, -1], [18, 16], [17, 8], [17, 6], [19, 5], [21, 5], [23, 7], [24, 10], [24, 12], [23, 15], [22, 17], [20, 19], [18, 20], [15, 21], [12, 21], [9, 20], [7, 19], [5, 17], [4, 15], [3, 12], [3, 9], [4, 6], [5, 4], [7, 2], [9, 1], [12, 0], [15, 0], [18, 1], [20, 2], [21, 3], [-1, -1], [19, 16], [18, 8], [18, 6], [19, 5] ] }, 'A': { width: 18, points: [ [9, 21], [1, 0], [-1, -1], [9, 21], [17, 0], [-1, -1], [4, 7], [14, 7] ] }, 'B': { width: 21, points: [ [4, 21], [4, 0], [-1, -1], [4, 21], [13, 21], [16, 20], [17, 19], [18, 17], [18, 15], [17, 13], [16, 12], [13, 11], [-1, -1], [4, 11], [13, 11], [16, 10], [17, 9], [18, 7], [18, 4], [17, 2], [16, 1], [13, 0], [4, 0] ] }, 'C': { width: 21, points: [ [18, 16], [17, 18], [15, 20], [13, 21], [9, 21], [7, 20], [5, 18], [4, 16], [3, 13], [3, 8], [4, 5], [5, 3], [7, 1], [9, 0], [13, 0], [15, 1], [17, 3], [18, 5] ] }, 'D': { width: 21, points: [ [4, 21], [4, 0], [-1, -1], [4, 21], [11, 21], [14, 20], [16, 18], [17, 16], [18, 13], [18, 8], [17, 5], [16, 3], [14, 1], [11, 0], [4, 0] ] }, 'E': { width: 19, points: [ [4, 21], [4, 0], [-1, -1], [4, 21], [17, 21], [-1, -1], [4, 11], [12, 11], [-1, -1], [4, 0], [17, 0] ] }, 'F': { width: 18, points: [ [4, 21], [4, 0], [-1, -1], [4, 21], [17, 21], [-1, -1], [4, 11], [12, 11] ] }, 'G': { width: 21, points: [ [18, 16], [17, 18], [15, 20], [13, 21], [9, 21], [7, 20], [5, 18], [4, 16], [3, 13], [3, 8], [4, 5], [5, 3], [7, 1], [9, 0], [13, 0], [15, 1], [17, 3], [18, 5], [18, 8], [-1, -1], [13, 8], [18, 8] ] }, 'H': { width: 22, points: [ [4, 21], [4, 0], [-1, -1], [18, 21], [18, 0], [-1, -1], [4, 11], [18, 11] ] }, 'I': { width: 8, points: [ [4, 21], [4, 0] ] }, 'J': { width: 16, points: [ [12, 21], [12, 5], [11, 2], [10, 1], [8, 0], [6, 0], [4, 1], [3, 2], [2, 5], [2, 7] ] }, 'K': { width: 21, points: [ [4, 21], [4, 0], [-1, -1], [18, 21], [4, 7], [-1, -1], [9, 12], [18, 0] ] }, 'L': { width: 17, points: [ [4, 21], [4, 0], [-1, -1], [4, 0], [16, 0] ] }, 'M': { width: 24, points: [ [4, 21], [4, 0], [-1, -1], [4, 21], [12, 0], [-1, -1], [20, 21], [12, 0], [-1, -1], [20, 21], [20, 0] ] }, 'N': { width: 22, points: [ [4, 21], [4, 0], [-1, -1], [4, 21], [18, 0], [-1, -1], [18, 21], [18, 0] ] }, 'O': { width: 22, points: [ [9, 21], [7, 20], [5, 18], [4, 16], [3, 13], [3, 8], [4, 5], [5, 3], [7, 1], [9, 0], [13, 0], [15, 1], [17, 3], [18, 5], [19, 8], [19, 13], [18, 16], [17, 18], [15, 20], [13, 21], [9, 21] ] }, 'P': { width: 21, points: [ [4, 21], [4, 0], [-1, -1], [4, 21], [13, 21], [16, 20], [17, 19], [18, 17], [18, 14], [17, 12], [16, 11], [13, 10], [4, 10] ] }, 'Q': { width: 22, points: [ [9, 21], [7, 20], [5, 18], [4, 16], [3, 13], [3, 8], [4, 5], [5, 3], [7, 1], [9, 0], [13, 0], [15, 1], [17, 3], [18, 5], [19, 8], [19, 13], [18, 16], [17, 18], [15, 20], [13, 21], [9, 21], [-1, -1], [12, 4], [18, -2] ] }, 'R': { width: 21, points: [ [4, 21], [4, 0], [-1, -1], [4, 21], [13, 21], [16, 20], [17, 19], [18, 17], [18, 15], [17, 13], [16, 12], [13, 11], [4, 11], [-1, -1], [11, 11], [18, 0] ] }, 'S': { width: 20, points: [ [17, 18], [15, 20], [12, 21], [8, 21], [5, 20], [3, 18], [3, 16], [4, 14], [5, 13], [7, 12], [13, 10], [15, 9], [16, 8], [17, 6], [17, 3], [15, 1], [12, 0], [8, 0], [5, 1], [3, 3] ] }, 'T': { width: 16, points: [ [8, 21], [8, 0], [-1, -1], [1, 21], [15, 21] ] }, 'U': { width: 22, points: [ [4, 21], [4, 6], [5, 3], [7, 1], [10, 0], [12, 0], [15, 1], [17, 3], [18, 6], [18, 21] ] }, 'V': { width: 18, points: [ [1, 21], [9, 0], [-1, -1], [17, 21], [9, 0] ] }, 'W': { width: 24, points: [ [2, 21], [7, 0], [-1, -1], [12, 21], [7, 0], [-1, -1], [12, 21], [17, 0], [-1, -1], [22, 21], [17, 0] ] }, 'X': { width: 20, points: [ [3, 21], [17, 0], [-1, -1], [17, 21], [3, 0] ] }, 'Y': { width: 18, points: [ [1, 21], [9, 11], [9, 0], [-1, -1], [17, 21], [9, 11] ] }, 'Z': { width: 20, points: [ [17, 21], [3, 0], [-1, -1], [3, 21], [17, 21], [-1, -1], [3, 0], [17, 0] ] }, '[': { width: 14, points: [ [4, 25], [4, -7], [-1, -1], [5, 25], [5, -7], [-1, -1], [4, 25], [11, 25], [-1, -1], [4, -7], [11, -7] ] }, '\\': { width: 14, points: [ [0, 21], [14, -3] ] }, ']': { width: 14, points: [ [9, 25], [9, -7], [-1, -1], [10, 25], [10, -7], [-1, -1], [3, 25], [10, 25], [-1, -1], [3, -7], [10, -7] ] }, '^': { width: 16, points: [ [6, 15], [8, 18], [10, 15], [-1, -1], [3, 12], [8, 17], [13, 12], [-1, -1], [8, 17], [8, 0] ] }, '_': { width: 16, points: [ [0, -2], [16, -2] ] }, '`': { width: 10, points: [ [6, 21], [5, 20], [4, 18], [4, 16], [5, 15], [6, 16], [5, 17] ] }, 'a': { width: 19, points: [ [15, 14], [15, 0], [-1, -1], [15, 11], [13, 13], [11, 14], [8, 14], [6, 13], [4, 11], [3, 8], [3, 6], [4, 3], [6, 1], [8, 0], [11, 0], [13, 1], [15, 3] ] }, 'b': { width: 19, points: [ [4, 21], [4, 0], [-1, -1], [4, 11], [6, 13], [8, 14], [11, 14], [13, 13], [15, 11], [16, 8], [16, 6], [15, 3], [13, 1], [11, 0], [8, 0], [6, 1], [4, 3] ] }, 'c': { width: 18, points: [ [15, 11], [13, 13], [11, 14], [8, 14], [6, 13], [4, 11], [3, 8], [3, 6], [4, 3], [6, 1], [8, 0], [11, 0], [13, 1], [15, 3] ] }, 'd': { width: 19, points: [ [15, 21], [15, 0], [-1, -1], [15, 11], [13, 13], [11, 14], [8, 14], [6, 13], [4, 11], [3, 8], [3, 6], [4, 3], [6, 1], [8, 0], [11, 0], [13, 1], [15, 3] ] }, 'e': { width: 18, points: [ [3, 8], [15, 8], [15, 10], [14, 12], [13, 13], [11, 14], [8, 14], [6, 13], [4, 11], [3, 8], [3, 6], [4, 3], [6, 1], [8, 0], [11, 0], [13, 1], [15, 3] ] }, 'f': { width: 12, points: [ [10, 21], [8, 21], [6, 20], [5, 17], [5, 0], [-1, -1], [2, 14], [9, 14] ] }, 'g': { width: 19, points: [ [15, 14], [15, -2], [14, -5], [13, -6], [11, -7], [8, -7], [6, -6], [-1, -1], [15, 11], [13, 13], [11, 14], [8, 14], [6, 13], [4, 11], [3, 8], [3, 6], [4, 3], [6, 1], [8, 0], [11, 0], [13, 1], [15, 3] ] }, 'h': { width: 19, points: [ [4, 21], [4, 0], [-1, -1], [4, 10], [7, 13], [9, 14], [12, 14], [14, 13], [15, 10], [15, 0] ] }, 'i': { width: 8, points: [ [3, 21], [4, 20], [5, 21], [4, 22], [3, 21], [-1, -1], [4, 14], [4, 0] ] }, 'j': { width: 10, points: [ [5, 21], [6, 20], [7, 21], [6, 22], [5, 21], [-1, -1], [6, 14], [6, -3], [5, -6], [3, -7], [1, -7] ] }, 'k': { width: 17, points: [ [4, 21], [4, 0], [-1, -1], [14, 14], [4, 4], [-1, -1], [8, 8], [15, 0] ] }, 'l': { width: 8, points: [ [4, 21], [4, 0] ] }, 'm': { width: 30, points: [ [4, 14], [4, 0], [-1, -1], [4, 10], [7, 13], [9, 14], [12, 14], [14, 13], [15, 10], [15, 0], [-1, -1], [15, 10], [18, 13], [20, 14], [23, 14], [25, 13], [26, 10], [26, 0] ] }, 'n': { width: 19, points: [ [4, 14], [4, 0], [-1, -1], [4, 10], [7, 13], [9, 14], [12, 14], [14, 13], [15, 10], [15, 0] ] }, 'o': { width: 19, points: [ [8, 14], [6, 13], [4, 11], [3, 8], [3, 6], [4, 3], [6, 1], [8, 0], [11, 0], [13, 1], [15, 3], [16, 6], [16, 8], [15, 11], [13, 13], [11, 14], [8, 14] ] }, 'p': { width: 19, points: [ [4, 14], [4, -7], [-1, -1], [4, 11], [6, 13], [8, 14], [11, 14], [13, 13], [15, 11], [16, 8], [16, 6], [15, 3], [13, 1], [11, 0], [8, 0], [6, 1], [4, 3] ] }, 'q': { width: 19, points: [ [15, 14], [15, -7], [-1, -1], [15, 11], [13, 13], [11, 14], [8, 14], [6, 13], [4, 11], [3, 8], [3, 6], [4, 3], [6, 1], [8, 0], [11, 0], [13, 1], [15, 3] ] }, 'r': { width: 13, points: [ [4, 14], [4, 0], [-1, -1], [4, 8], [5, 11], [7, 13], [9, 14], [12, 14] ] }, 's': { width: 17, points: [ [14, 11], [13, 13], [10, 14], [7, 14], [4, 13], [3, 11], [4, 9], [6, 8], [11, 7], [13, 6], [14, 4], [14, 3], [13, 1], [10, 0], [7, 0], [4, 1], [3, 3] ] }, 't': { width: 12, points: [ [5, 21], [5, 4], [6, 1], [8, 0], [10, 0], [-1, -1], [2, 14], [9, 14] ] }, 'u': { width: 19, points: [ [4, 14], [4, 4], [5, 1], [7, 0], [10, 0], [12, 1], [15, 4], [-1, -1], [15, 14], [15, 0] ] }, 'v': { width: 16, points: [ [2, 14], [8, 0], [-1, -1], [14, 14], [8, 0] ] }, 'w': { width: 22, points: [ [3, 14], [7, 0], [-1, -1], [11, 14], [7, 0], [-1, -1], [11, 14], [15, 0], [-1, -1], [19, 14], [15, 0] ] }, 'x': { width: 17, points: [ [3, 14], [14, 0], [-1, -1], [14, 14], [3, 0] ] }, 'y': { width: 16, points: [ [2, 14], [8, 0], [-1, -1], [14, 14], [8, 0], [6, -4], [4, -6], [2, -7], [1, -7] ] }, 'z': { width: 17, points: [ [14, 14], [3, 0], [-1, -1], [3, 14], [14, 14], [-1, -1], [3, 0], [14, 0] ] }, '{': { width: 14, points: [ [9, 25], [7, 24], [6, 23], [5, 21], [5, 19], [6, 17], [7, 16], [8, 14], [8, 12], [6, 10], [-1, -1], [7, 24], [6, 22], [6, 20], [7, 18], [8, 17], [9, 15], [9, 13], [8, 11], [4, 9], [8, 7], [9, 5], [9, 3], [8, 1], [7, 0], [6, -2], [6, -4], [7, -6], [-1, -1], [6, 8], [8, 6], [8, 4], [7, 2], [6, 1], [5, -1], [5, -3], [6, -5], [7, -6], [9, -7] ] }, '|': { width: 8, points: [ [4, 25], [4, -7] ] }, '}': { width: 14, points: [ [5, 25], [7, 24], [8, 23], [9, 21], [9, 19], [8, 17], [7, 16], [6, 14], [6, 12], [8, 10], [-1, -1], [7, 24], [8, 22], [8, 20], [7, 18], [6, 17], [5, 15], [5, 13], [6, 11], [10, 9], [6, 7], [5, 5], [5, 3], [6, 1], [7, 0], [8, -2], [8, -4], [7, -6], [-1, -1], [8, 8], [6, 6], [6, 4], [7, 2], [8, 1], [9, -1], [9, -3], [8, -5], [7, -6], [5, -7] ] }, '~': { width: 24, points: [ [3, 6], [3, 8], [4, 11], [6, 12], [8, 12], [10, 11], [14, 8], [16, 7], [18, 7], [20, 8], [21, 10], [-1, -1], [3, 8], [4, 10], [6, 11], [8, 11], [10, 10], [14, 7], [16, 6], [18, 6], [20, 7], [21, 10], [21, 12] ] } }; /** * @desc Creates wireframe vector text {@link Geometry}. * * ## Usage * * Creating a {@link Mesh} with vector text {@link ReadableGeometry} : * * [[Run this example](/examples/index.html#geometry_builders_buildVectorTextGeometry)] * * ````javascript * * import {Viewer, Mesh, buildVectorTextGeometry, ReadableGeometry, PhongMaterial} from "xeokit-sdk.es.js"; * * const viewer = new Viewer({ * canvasId: "myCanvas" * }); * * viewer.camera.eye = [0, 0, 100]; * viewer.camera.look = [0, 0, 0]; * viewer.camera.up = [0, 1, 0]; * * new Mesh(viewer.scene, { * geometry: new ReadableGeometry(viewer.scene, buildVectorTextGeometry({ * origin: [0,0,0], * text: "On the other side of the screen, it all looked so easy" * }), * material: new PhongMaterial(viewer.scene, { * diffuseMap: new Texture(viewer.scene, { * src: "textures/diffuse/uvGrid2.jpg" * }) * }) * }); * ```` * * @function buildVectorTextGeometry * @param {*} [cfg] Configs * @param {String} [cfg.id] Optional ID, unique among all components in the parent {@link Scene}, generated automatically when omitted. * @param {Number[]} [cfg.center] 3D point indicating the center position. * @param {Number[]} [cfg.origin] 3D point indicating the top left corner. * @param {Number} [cfg.size=1] Size of each character. * @param {String} [cfg.text=""] The text. * @returns {Object} Configuration for a {@link Geometry} subtype. */ function buildVectorTextGeometry(cfg = {}) { var origin = cfg.origin || [0, 0, 0]; var xOrigin = origin[0]; var yOrigin = origin[1]; var zOrigin = origin[2]; var size = cfg.size || 1; var positions = []; var indices = []; var text = cfg.text; if (utils.isNumeric(text)) { text = "" + text; } var lines = (text || "").split("\n"); var countVerts = 0; var y = 0; var x; var str; var len; var c; var mag = 1.0 / 25.0; var penUp; var p1; var p2; var pointsLen; var a; for (var iLine = 0; iLine < lines.length; iLine++) { x = 0; str = lines[iLine]; len = str.length; for (var i = 0; i < len; i++) { c = letters[str.charAt(i)]; if (!c) { continue; } penUp = 1; p1 = -1; p2 = -1; pointsLen = c.points.length; for (var j = 0; j < pointsLen; j++) { a = c.points[j]; if (a[0] === -1 && a[1] === -1) { penUp = 1; continue; } positions.push((x + (a[0] * size) * mag) + xOrigin); positions.push((y + (a[1] * size) * mag) + yOrigin); positions.push(0 + zOrigin); if (p1 === -1) { p1 = countVerts; } else if (p2 === -1) { p2 = countVerts; } else { p1 = p2; p2 = countVerts; } countVerts++; if (penUp) { penUp = false; } else { indices.push(p1); indices.push(p2); } } x += c.width * mag * size; } y -= 35 * mag * size; } return utils.apply(cfg, { primitive: "lines", positions: positions, indices: indices }); } /** * @desc Creates a 3D polyline {@link Geometry}. * * ## Usage * * In the example below we'll create a {@link Mesh} with a polyline {@link ReadableGeometry} that has lines primitives. * * [[Run this example](https://xeokit.github.io/xeokit-sdk/examples/scenegraph/#buildPolylineGeometry)] * * ````javascript * //------------------------------------------------------------------------------------------------------------------ * // Import the modules we need for this example * //------------------------------------------------------------------------------------------------------------------ * * import {buildPolylineGeometry, Viewer, Mesh, ReadableGeometry, PhongMaterial} from "../../dist/xeokit-sdk.min.es.js"; * * //------------------------------------------------------------------------------------------------------------------ * // Create a Viewer and arrange the camera * //------------------------------------------------------------------------------------------------------------------ * * const viewer = new Viewer({ * canvasId: "myCanvas" * }); * * viewer.camera.eye = [0, 0, 8]; * viewer.camera.look = [0, 0, 0]; * viewer.camera.up = [0, 1, 0]; * * //------------------------------------------------------------------------------------------------------------------ * // Create a mesh with polyline shape * //------------------------------------------------------------------------------------------------------------------ * * new Mesh(viewer.scene, { * geometry: new ReadableGeometry(viewer.scene, buildPolylineGeometry({ * points: [ * 0, 2.83654, 0, * -0.665144, 1.152063, 0, * -2.456516, 1.41827, 0, * -1.330288, 0, 0, * -2.456516, -1.41827, 0, * -0.665144, -1.152063, 0, * 0, -2.83654, 0, * 0.665144, -1.152063, 0, * 2.456516, -1.41827, 0, * 1.330288, 0, 0, * 2.456516, 1.41827, 0, * 0.665144, 1.152063, 0, * 0, 2.83654, 0, * ] * })), * material: new PhongMaterial(viewer.scene, { * emissive: [0, 1,] * }) * }); * ```` * * @function buildPolylineGeometry * @param {*} [cfg] Configs * @param {String} [cfg.id] Optional ID, unique among all components in the parent {@link Scene}, generated automatically when omitted. * @param {Number[]} [cfg.points] 3D points indicating vertices position. * @returns {Object} Configuration for a {@link Geometry} subtype. */ function buildPolylineGeometry(cfg = {}) { if (cfg.points.length % 3 !== 0) { throw "Size of points array for given polyline should be divisible by 3"; } let numberOfPoints = cfg.points.length / 3; if (numberOfPoints < 2) { throw "There should be at least 2 points to create a polyline"; } let indices = []; for (let i = 0; i < numberOfPoints - 1; i++) { indices.push(i); indices.push(i + 1); } return utils.apply(cfg, { primitive: "lines", positions: cfg.points, indices: indices, }); } /** * @desc Creates a 3D polyline from curve {@link Geometry}. * * ## Usage * * In the example below we'll create a {@link Mesh} with a polyline {@link ReadableGeometry} created from curves. * * [[Run this example](https://xeokit.github.io/xeokit-sdk/examples/scenegraph/#buildPolylineGeometryFromCurve)] * * ````javascript * //------------------------------------------------------------------------------------------------------------------ * // Import the modules we need for this example * //------------------------------------------------------------------------------------------------------------------ * * import {buildPolylineGeometryFromCurve, Viewer, Mesh, PhongMaterial, NavCubePlugin, CubicBezierCurve, SplineCurve, QuadraticBezierCurve, ReadableGeometry} from "../../dist/xeokit-sdk.min.es.js"; * * //------------------------------------------------------------------------------------------------------------------ * // Create a Viewer and arrange the camera * //------------------------------------------------------------------------------------------------------------------ * * const viewer = new Viewer({ * canvasId: "myCanvas" * }); * * new NavCubePlugin(viewer, { * canvasId: "myNavCubeCanvas", * visible: true, * size: 250, * alignment: "bottomRight", * bottomMargin: 100, * rightMargin: 10 * }); * * viewer.camera.eye = [0, -250, 1]; * viewer.camera.look = [0, 0, 0]; * viewer.camera.up = [0, 1, 0]; * * //------------------------------------------------------------------------------------------------------------------ * // Create a mesh with polyline shape from Spline * //------------------------------------------------------------------------------------------------------------------ * * new Mesh(viewer.scene, { * geometry: new ReadableGeometry(viewer.scene, buildPolylineGeometryFromCurve({ * id: "SplineCurve", * curve: new SplineCurve(viewer.scene, { * points: [ * [-65.77614, 0, -88.881992], * [90.020852, 0, -61.589088], * [-67.766247, 0, -22.071238], * [93.148164, 0, -13.826507], * [-14.033343, 0, 3.231558], * [32.592034, 0, 9.20188], * [3.309023, 0, 22.848332], * [23.210098, 0, 28.818655], * ], * }), * divisions: 100, * })), * material: new PhongMaterial(viewer.scene, { * emissive: [1, 0, 0] * }) * }); * * //------------------------------------------------------------------------------------------------------------------ * // Create a mesh with polyline shape from CubicBezier * //------------------------------------------------------------------------------------------------------------------ * * new Mesh(viewer.scene, { * geometry: new ReadableGeometry(viewer.scene, buildPolylineGeometryFromCurve({ * id: "CubicBezierCurve", * curve: new CubicBezierCurve(viewer.scene, { * v0: [120, 0, 100], * v1: [120, 0, 0], * v2: [80, 0, 100], * v3: [80, 0, 0], * }), * divisions: 50, * })), * material: new PhongMaterial(viewer.scene, { * emissive: [0, 1, 0] * }) * }); * * //------------------------------------------------------------------------------------------------------------------ * // Create a mesh with polyline shape from QuadraticBezier * //------------------------------------------------------------------------------------------------------------------ * * new Mesh(viewer.scene, { * geometry: new ReadableGeometry(viewer.scene, buildPolylineGeometryFromCurve({ * id: "QuadraticBezierCurve", * curve: new QuadraticBezierCurve(viewer.scene, { * v0: [-100, 0, 100], * v1: [-50, 0, 150], * v2: [-50, 0, 0], * }), * divisions: 20, * })), * material: new PhongMaterial(viewer.scene, { * emissive: [0, 0, 1] * }) * }); * ```` * * @function buildPolylineGeometryFromCurve * @param {*} [cfg] Configs * @param {String} [cfg.id] Optional ID, unique among all components in the parent {@link Scene}, generated automatically when omitted. * @param {Object} [cfg.curve] Curve for which polyline will be created. * @param {Number} [cfg.divisions] The number of divisions. * @returns {Object} Configuration for a {@link Geometry} subtype. */ function buildPolylineGeometryFromCurve(cfg = {}) { let polylinePoints = cfg.curve.getPoints(cfg.divisions).map(a => [...a]).flat(); return buildPolylineGeometry({ id: cfg.id, points: polylinePoints }); } /** * @desc Creates a 3D line {@link Geometry}. * * ## Usage * * In the example below we'll create a {@link Mesh} with a line {@link ReadableGeometry}. * * [[Run this example](https://xeokit.github.io/xeokit-sdk/examples/scenegraph/#buildLineGeometry)] * * ````javascript * //------------------------------------------------------------------------------------------------------------------ * // Import the modules we need for this example * //------------------------------------------------------------------------------------------------------------------ * * import {buildLineGeometry, Viewer, Mesh, ReadableGeometry, PhongMaterial} from "../../dist/xeokit-sdk.min.es.js"; * * //------------------------------------------------------------------------------------------------------------------ * // Create a Viewer and arrange the camera * //------------------------------------------------------------------------------------------------------------------ * * const viewer = new Viewer({ * canvasId: "myCanvas" * }); * * viewer.camera.eye = [0, 0, 8]; * viewer.camera.look = [0, 0, 0]; * viewer.camera.up = [0, 1, 0]; * * //------------------------------------------------------------------------------------------------------------------ * // Create a mesh with simple 2d line shape * //------------------------------------------------------------------------------------------------------------------ * * new Mesh(viewer.scene, { * geometry: new ReadableGeometry(viewer.scene, buildLineGeometry({ * startPoint: [-5,-2,0], * endPoint: [-5,2,0], * })), * material: new PhongMaterial(viewer.scene, { * emissive: [0, 1,] * }) * }); * * //------------------------------------------------------------------------------------------------------------------ * // Create a mesh with simple 2d line shape with black color * //------------------------------------------------------------------------------------------------------------------ * * new Mesh(viewer.scene, { * geometry: new ReadableGeometry(viewer.scene, buildLineGeometry({ * startPoint: [-4,-2,0], * endPoint: [-4,2,0], * })), * material: new PhongMaterial(viewer.scene, { * emissive: [0, 0, 0] * }) * }); * * //------------------------------------------------------------------------------------------------------------------ * // Create a mesh with simple 2d line shape with black color and simple pattern * //------------------------------------------------------------------------------------------------------------------ * * new Mesh(viewer.scene, { * geometry: new ReadableGeometry(viewer.scene, buildLineGeometry({ * startPoint: [-3,-2,0], * endPoint: [-3,2,0], * pattern: [0.10], * })), * material: new PhongMaterial(viewer.scene, { * emissive: [0, 0, 0] * }) * }); * * //------------------------------------------------------------------------------------------------------------------ * // Create a mesh with simple 2d line shape with blue color and simple pattern extended to end * //------------------------------------------------------------------------------------------------------------------ * * new Mesh(viewer.scene, { * geometry: new ReadableGeometry(viewer.scene, buildLineGeometry({ * startPoint: [-2,-2,0], * endPoint: [-2,2,0], * pattern: [0.10], * extendToEnd: true, * })), * material: new PhongMaterial(viewer.scene, { * emissive: [0, 0, 1] * }) * }); * * //------------------------------------------------------------------------------------------------------------------ * // Create a mesh with simple 2d line shape with black color and more complex pattern * //------------------------------------------------------------------------------------------------------------------ * * new Mesh(viewer.scene, { * geometry: new ReadableGeometry(viewer.scene, buildLineGeometry({ * startPoint: [-1,-2,0], * endPoint: [-1,2,0], * pattern: [0.15, 0.05], * })), * material: new PhongMaterial(viewer.scene, { * emissive: [0, 0, 0] * }) * }); * * //------------------------------------------------------------------------------------------------------------------ * // Create a mesh with simple 2d line shape with blue color and more complex pattern extended to end * //------------------------------------------------------------------------------------------------------------------ * * new Mesh(viewer.scene, { * geometry: new ReadableGeometry(viewer.scene, buildLineGeometry({ * startPoint: [0,-2,0], * endPoint: [0,2,0], * pattern: [0.15, 0.05], * extendToEnd: true, * })), * material: new PhongMaterial(viewer.scene, { * emissive: [0, 0, 1] * }) * }); * * //------------------------------------------------------------------------------------------------------------------ * // Create a mesh with simple 2d line shape with black color and complex pattern * //------------------------------------------------------------------------------------------------------------------ * * new Mesh(viewer.scene, { * geometry: new ReadableGeometry(viewer.scene, buildLineGeometry({ * startPoint: [1,-2,0], * endPoint: [1,2,0], * pattern: [0.15, 0.05, 0.50], * })), * material: new PhongMaterial(viewer.scene, { * emissive: [0, 0, 0] * }) * }); * * //------------------------------------------------------------------------------------------------------------------ * // Create a mesh with simple 2d line shape with blue color and complex pattern extended to end * //------------------------------------------------------------------------------------------------------------------ * * new Mesh(viewer.scene, { * geometry: new ReadableGeometry(viewer.scene, buildLineGeometry({ * startPoint: [2,-2,0], * endPoint: [2,2,0], * pattern: [0.15, 0.05, 0.50], * extendToEnd: true, * })), * material: new PhongMaterial(viewer.scene, { * emissive: [0, 0, 1] * }) * }); * * //------------------------------------------------------------------------------------------------------------------ * // Create a mesh with simple 3d line shape with white color and simple pattern * //------------------------------------------------------------------------------------------------------------------ * * new Mesh(viewer.scene, { * geometry: new ReadableGeometry(viewer.scene, buildLineGeometry({ * startPoint: [3,-2,-1], * endPoint: [5,2,1], * pattern: [0.10], * })), * material: new PhongMaterial(viewer.scene, { * emissive: [1, 1, 1] * }) * }); * * //------------------------------------------------------------------------------------------------------------------ * // Create a mesh with simple 3d line shape with black color and simple dot pattern * //------------------------------------------------------------------------------------------------------------------ * * new Mesh(viewer.scene, { * geometry: new ReadableGeometry(viewer.scene, buildLineGeometry({ * startPoint: [5,-2,-1], * endPoint: [7,2,1], * pattern: [0.03], * })), * material: new PhongMaterial(viewer.scene, { * emissive: [0, 0, 0] * }) * }); * ```` * * @function buildLineGeometry * @param {*} [cfg] Configs * @param {String} [cfg.id] Optional ID, unique among all components in the parent {@link Scene}, generated automatically when omitted. * @param {Number[]} [cfg.startPoint] 3D start point (x0, y0, z0). * @param {Number[]} [cfg.endPoint] 3D end point (x1, y1, z1). * @param {Number[]} [cfg.pattern] Lengths of segments that describe a pattern. * @param {Bool} [cfg.extendToEnd] If true: it will try to make sure the line doesn't end up with a gap, as it will * extend the last segment. * @returns {Object} Configuration for a {@link Geometry} subtype. */ function buildLineGeometry(cfg = {}) { if (cfg.startPoint.length !== 3) { throw "Start point should contain 3 elements in array: x, y and z"; } if (cfg.endPoint.length !== 3) { throw "End point should contain 3 elements in array: x, y and z"; } let indices = []; let points = []; let x0 = cfg.startPoint[0]; let y0 = cfg.startPoint[1]; let z0 = cfg.startPoint[2]; let x1 = cfg.endPoint[0]; let y1 = cfg.endPoint[1]; let z1 = cfg.endPoint[2]; let lineLength = Math.sqrt((x1- x0)**2 + (y1 - y0)**2 + (z1 - z0)**2); let normalizedDirectionVectorOfLine = [(x1-x0)/lineLength, (y1-y0)/lineLength, (z1-z0)/lineLength]; if (!cfg.pattern) { indices.push(0); indices.push(1); points.push(x0, y0, z0, x1, y1, z1); } else { let patternsNumber = cfg.pattern.length; let gap = false; let segmentFilled = 0.0; let idOfCurrentPatternLength = 0; let pointIndicesCounter = 0; let currentStartPoint = [x0, y0, z0]; let currentPatternLength = cfg.pattern[idOfCurrentPatternLength]; points.push(currentStartPoint[0], currentStartPoint[1], currentStartPoint[2]); while (currentPatternLength <= (lineLength - segmentFilled)) { let vectorFromCurrentStartPointToCurrentEndPoint = [ normalizedDirectionVectorOfLine[0] * currentPatternLength, normalizedDirectionVectorOfLine[1] * currentPatternLength, normalizedDirectionVectorOfLine[2] * currentPatternLength, ]; let currentEndPoint = [ currentStartPoint[0] + vectorFromCurrentStartPointToCurrentEndPoint[0], currentStartPoint[1] + vectorFromCurrentStartPointToCurrentEndPoint[1], currentStartPoint[2] + vectorFromCurrentStartPointToCurrentEndPoint[2], ]; points.push(currentEndPoint[0], currentEndPoint[1], currentEndPoint[2]); if (!gap) { indices.push(pointIndicesCounter); indices.push(pointIndicesCounter + 1); } gap = !gap; pointIndicesCounter += 1; currentStartPoint = currentEndPoint; idOfCurrentPatternLength += 1; if (idOfCurrentPatternLength >= patternsNumber) { idOfCurrentPatternLength = 0; } segmentFilled += currentPatternLength; currentPatternLength = cfg.pattern[idOfCurrentPatternLength]; } if (cfg.extendToEnd) { points.push(x1, y1, z1); indices.push(indices.length - 2); indices.push(indices.length - 1); } } return utils.apply(cfg, { primitive: "lines", positions: points, indices: indices, }); } const angleAxis$2 = math.vec4(4); const q1$2 = math.vec4(); const q2$2 = math.vec4(); const xAxis$2 = math.vec3([1, 0, 0]); const yAxis$2 = math.vec3([0, 1, 0]); const zAxis$2 = math.vec3([0, 0, 1]); const veca$1 = math.vec3(3); const vecb$1 = math.vec3(3); const identityMat$2 = math.identityMat4(); /** * @desc An {@link Entity} that is a scene graph node that can have child Nodes and {@link Mesh}es. * * ## Usage * * The example below is the same as the one given for {@link Mesh}, since the two classes work together. In this example, * we'll create a scene graph in which a root Node represents a group and the {@link Mesh}s are leaves. Since Node * implements {@link Entity}, we can designate the root Node as a model, causing it to be registered by its ID in {@link Scene#models}. * * Since {@link Mesh} also implements {@link Entity}, we can designate the leaf {@link Mesh}es as objects, causing them to * be registered by their IDs in {@link Scene#objects}. * * We can then find those {@link Entity} types in {@link Scene#models} and {@link Scene#objects}. * * We can also update properties of our object-Meshes via calls to {@link Scene#setObjectsHighlighted} etc. * * [[Run this example](/examples/index.html#sceneRepresentation_SceneGraph)] * * ````javascript * import {Viewer, Mesh, Node, PhongMaterial} from "xeokit-sdk.es.js"; * * const viewer = new Viewer({ * canvasId: "myCanvas" * }); * * viewer.scene.camera.eye = [-21.80, 4.01, 6.56]; * viewer.scene.camera.look = [0, -5.75, 0]; * viewer.scene.camera.up = [0.37, 0.91, -0.11]; * * new Node(viewer.scene, { * id: "table", * isModel: true, // <---------- Node represents a model, so is registered by ID in viewer.scene.models * rotation: [0, 50, 0], * position: [0, 0, 0], * scale: [1, 1, 1], * * children: [ * * new Mesh(viewer.scene, { // Red table leg * id: "redLeg", * isObject: true, // <------ Node represents an object, so is registered by ID in viewer.scene.objects * position: [-4, -6, -4], * scale: [1, 3, 1], * rotation: [0, 0, 0], * material: new PhongMaterial(viewer.scene, { * diffuse: [1, 0.3, 0.3] * }) * }), * * new Mesh(viewer.scene, { // Green table leg * id: "greenLeg", * isObject: true, // <------ Node represents an object, so is registered by ID in viewer.scene.objects * position: [4, -6, -4], * scale: [1, 3, 1], * rotation: [0, 0, 0], * material: new PhongMaterial(viewer.scene, { * diffuse: [0.3, 1.0, 0.3] * }) * }), * * new Mesh(viewer.scene, {// Blue table leg * id: "blueLeg", * isObject: true, // <------ Node represents an object, so is registered by ID in viewer.scene.objects * position: [4, -6, 4], * scale: [1, 3, 1], * rotation: [0, 0, 0], * material: new PhongMaterial(viewer.scene, { * diffuse: [0.3, 0.3, 1.0] * }) * }), * * new Mesh(viewer.scene, { // Yellow table leg * id: "yellowLeg", * isObject: true, // <------ Node represents an object, so is registered by ID in viewer.scene.objects * position: [-4, -6, 4], * scale: [1, 3, 1], * rotation: [0, 0, 0], * material: new PhongMaterial(viewer.scene, { * diffuse: [1.0, 1.0, 0.0] * }) * }), * * new Mesh(viewer.scene, { // Purple table top * id: "tableTop", * isObject: true, // <------ Node represents an object, so is registered by ID in viewer.scene.objects * position: [0, -3, 0], * scale: [6, 0.5, 6], * rotation: [0, 0, 0], * material: new PhongMaterial(viewer.scene, { * diffuse: [1.0, 0.3, 1.0] * }) * }) * ] * }); * * // Find Nodes and Meshes by their IDs * * var table = viewer.scene.models["table"]; // Since table Node has isModel == true * * var redLeg = viewer.scene.objects["redLeg"]; // Since the Meshes have isObject == true * var greenLeg = viewer.scene.objects["greenLeg"]; * var blueLeg = viewer.scene.objects["blueLeg"]; * * // Highlight one of the table leg Meshes * * viewer.scene.setObjectsHighlighted(["redLeg"], true); // Since the Meshes have isObject == true * * // Periodically update transforms on our Nodes and Meshes * * viewer.scene.on("tick", function () { * * // Rotate legs * redLeg.rotateY(0.5); * greenLeg.rotateY(0.5); * blueLeg.rotateY(0.5); * * // Rotate table * table.rotateY(0.5); * table.rotateX(0.3); * }); * ```` * * ## Metadata * * As mentioned, we can also associate {@link MetaModel}s and {@link MetaObject}s with our Nodes and {@link Mesh}es, * within a {@link MetaScene}. See {@link MetaScene} for an example. * * @implements {Entity} */ class Node$1 extends Component { /** * @constructor * @param {Component} owner Owner component. When destroyed, the owner will destroy this component as well. * @param {*} [cfg] Configs * @param {String} [cfg.id] Optional ID, unique among all components in the parent scene, generated automatically when omitted. * @param {Boolean} [cfg.isModel] Specify ````true```` if this Mesh represents a model, in which case the Mesh will be registered by {@link Mesh#id} in {@link Scene#models} and may also have a corresponding {@link MetaModel} with matching {@link MetaModel#id}, registered by that ID in {@link MetaScene#metaModels}. * @param {Boolean} [cfg.isObject] Specify ````true```` if this Mesh represents an object, in which case the Mesh will be registered by {@link Mesh#id} in {@link Scene#objects} and may also have a corresponding {@link MetaObject} with matching {@link MetaObject#id}, registered by that ID in {@link MetaScene#metaObjects}. * @param {Node} [cfg.parent] The parent Node. * @param {Number[]} [cfg.origin] World-space origin for this Node. * @param {Number[]} [cfg.rtcCenter] Deprecated - renamed to ````origin````. * @param {Number[]} [cfg.position=[0,0,0]] Local 3D position. * @param {Number[]} [cfg.scale=[1,1,1]] Local scale. * @param {Number[]} [cfg.rotation=[0,0,0]] Local rotation, as Euler angles given in degrees, for each of the X, Y and Z axis. * @param {Number[]} [cfg.matrix=[1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1] Local modelling transform matrix. Overrides the position, scale and rotation parameters. * @param {Number[]} [cfg.offset=[0,0,0]] World-space 3D translation offset. Translates the Node in World space, after modelling transforms. * @param {Boolean} [cfg.visible=true] Indicates if the Node is initially visible. * @param {Boolean} [cfg.culled=false] Indicates if the Node is initially culled from view. * @param {Boolean} [cfg.pickable=true] Indicates if the Node is initially pickable. * @param {Boolean} [cfg.clippable=true] Indicates if the Node is initially clippable. * @param {Boolean} [cfg.collidable=true] Indicates if the Node is initially included in boundary calculations. * @param {Boolean} [cfg.castsShadow=true] Indicates if the Node initially casts shadows. * @param {Boolean} [cfg.receivesShadow=true] Indicates if the Node initially receives shadows. * @param {Boolean} [cfg.xrayed=false] Indicates if the Node is initially xrayed. * @param {Boolean} [cfg.highlighted=false] Indicates if the Node is initially highlighted. * @param {Boolean} [cfg.selected=false] Indicates if the Mesh is initially selected. * @param {Boolean} [cfg.edges=false] Indicates if the Node's edges are initially emphasized. * @param {Number[]} [cfg.colorize=[1.0,1.0,1.0]] Node's initial RGB colorize color, multiplies by the rendered fragment colors. * @param {Number} [cfg.opacity=1.0] Node's initial opacity factor, multiplies by the rendered fragment alpha. * @param {Array} [cfg.children] Child Nodes or {@link Mesh}es to add initially. Children must be in the same {@link Scene} and will be removed first from whatever parents they may already have. * @param {Boolean} [cfg.inheritStates=true] Indicates if children given to this constructor should inherit rendering state from this parent as they are added. Rendering state includes {@link Node#visible}, {@link Node#culled}, {@link Node#pickable}, {@link Node#clippable}, {@link Node#castsShadow}, {@link Node#receivesShadow}, {@link Node#selected}, {@link Node#highlighted}, {@link Node#colorize} and {@link Node#opacity}. */ constructor(owner, cfg = {}) { super(owner, cfg); this._parentNode = null; this._children = []; this._aabb = null; this._aabbDirty = true; this.scene._aabbDirty = true; this._numTriangles = 0; this._scale = math.vec3(); this._quaternion = math.identityQuaternion(); this._rotation = math.vec3(); this._position = math.vec3(); this._offset = math.vec3(); this._localMatrix = math.identityMat4(); this._worldMatrix = math.identityMat4(); this._localMatrixDirty = true; this._worldMatrixDirty = true; if (cfg.matrix) { this.matrix = cfg.matrix; } else { this.scale = cfg.scale; this.position = cfg.position; if (cfg.quaternion) ; else { this.rotation = cfg.rotation; } } this._isModel = cfg.isModel; if (this._isModel) { this.scene._registerModel(this); } this._isObject = cfg.isObject; if (this._isObject) { this.scene._registerObject(this); } this.origin = cfg.origin; this.visible = cfg.visible; this.culled = cfg.culled; this.pickable = cfg.pickable; this.clippable = cfg.clippable; this.collidable = cfg.collidable; this.castsShadow = cfg.castsShadow; this.receivesShadow = cfg.receivesShadow; this.xrayed = cfg.xrayed; this.highlighted = cfg.highlighted; this.selected = cfg.selected; this.edges = cfg.edges; this.colorize = cfg.colorize; this.opacity = cfg.opacity; this.offset = cfg.offset; // Add children, which inherit state from this Node if (cfg.children) { const children = cfg.children; for (let i = 0, len = children.length; i < len; i++) { this.addChild(children[i], cfg.inheritStates); } } if (cfg.parentId) { const parentNode = this.scene.components[cfg.parentId]; if (!parentNode) { this.error("Parent not found: '" + cfg.parentId + "'"); } else if (!parentNode.isNode) { this.error("Parent is not a Node: '" + cfg.parentId + "'"); } else { parentNode.addChild(this); } } else if (cfg.parent) { if (!cfg.parent.isNode) { this.error("Parent is not a Node"); } cfg.parent.addChild(this); } } //------------------------------------------------------------------------------------------------------------------ // Entity members //------------------------------------------------------------------------------------------------------------------ /** * Returns true to indicate that this Component is an Entity. * @type {Boolean} */ get isEntity() { return true; } /** * Returns ````true```` if this Mesh represents a model. * * When this returns ````true````, the Mesh will be registered by {@link Mesh#id} in {@link Scene#models} and * may also have a corresponding {@link MetaModel}. * * @type {Boolean} */ get isModel() { return this._isModel; } /** * Returns ````true```` if this Node represents an object. * * When ````true```` the Node will be registered by {@link Node#id} in * {@link Scene#objects} and may also have a {@link MetaObject} with matching {@link MetaObject#id}. * * @type {Boolean} * @abstract */ get isObject() { return this._isObject; } /** * Gets the Node's World-space 3D axis-aligned bounding box. * * Represented by a six-element Float64Array containing the min/max extents of the * axis-aligned volume, ie. ````[xmin, ymin,zmin,xmax,ymax, zmax]````. * * @type {Number[]} */ get aabb() { if (this._aabbDirty) { this._updateAABB(); } return this._aabb; } /** * Sets the World-space origin for this Node. * * @type {Float64Array} */ set origin(origin) { if (origin) { if (!this._origin) { this._origin = math.vec3(); } this._origin.set(origin); } else { if (this._origin) { this._origin = null; } } for (let i = 0, len = this._children.length; i < len; i++) { this._children[i].origin = origin; } this.glRedraw(); } /** * Gets the World-space origin for this Node. * * @type {Float64Array} */ get origin() { return this._origin; } /** * Sets the World-space origin for this Node. * * Deprecated and replaced by {@link Node#origin}. * * @deprecated * @type {Float64Array} */ set rtcCenter(rtcCenter) { this.origin = rtcCenter; } /** * Gets the World-space origin for this Node. * * Deprecated and replaced by {@link Node#origin}. * * @deprecated * @type {Float64Array} */ get rtcCenter() { return this.origin; } /** * The number of triangles in this Node. * * @type {Number} */ get numTriangles() { return this._numTriangles; } /** * Sets if this Node and all child Nodes and {@link Mesh}es are visible. * * Only rendered both {@link Node#visible} is ````true```` and {@link Node#culled} is ````false````. * * When {@link Node#isObject} and {@link Node#visible} are both ````true```` the Node will be * registered by {@link Node#id} in {@link Scene#visibleObjects}. * * @type {Boolean} */ set visible(visible) { visible = visible !== false; this._visible = visible; for (let i = 0, len = this._children.length; i < len; i++) { this._children[i].visible = visible; } if (this._isObject) { this.scene._objectVisibilityUpdated(this, visible); } } /** * Gets if this Node is visible. * * Child Nodes and {@link Mesh}es may have different values for this property. * * When {@link Node#isObject} and {@link Node#visible} are both ````true```` the Node will be * registered by {@link Node#id} in {@link Scene#visibleObjects}. * * @type {Boolean} */ get visible() { return this._visible; } /** * Sets if this Node and all child Nodes and {@link Mesh}es are xrayed. * * When {@link Node#isObject} and {@link Node#xrayed} are both ````true```` the Node will be * registered by {@link Node#id} in {@link Scene#xrayedObjects}. * * @type {Boolean} */ set xrayed(xrayed) { xrayed = !!xrayed; this._xrayed = xrayed; for (let i = 0, len = this._children.length; i < len; i++) { this._children[i].xrayed = xrayed; } if (this._isObject) { this.scene._objectXRayedUpdated(this, xrayed); } } /** * Gets if this Node is xrayed. * * When {@link Node#isObject} and {@link Node#xrayed} are both ````true```` the Node will be * registered by {@link Node#id} in {@link Scene#xrayedObjects}. * * Child Nodes and {@link Mesh}es may have different values for this property. * * @type {Boolean} */ get xrayed() { return this._xrayed; } /** * Sets if this Node and all child Nodes and {@link Mesh}es are highlighted. * * When {@link Node#isObject} and {@link Node#highlighted} are both ````true```` the Node will be * registered by {@link Node#id} in {@link Scene#highlightedObjects}. * * @type {Boolean} */ set highlighted(highlighted) { highlighted = !!highlighted; this._highlighted = highlighted; for (let i = 0, len = this._children.length; i < len; i++) { this._children[i].highlighted = highlighted; } if (this._isObject) { this.scene._objectHighlightedUpdated(this, highlighted); } } /** * Gets if this Node is highlighted. * * When {@link Node#isObject} and {@link Node#highlighted} are both ````true```` the Node will be * registered by {@link Node#id} in {@link Scene#highlightedObjects}. * * Child Nodes and {@link Mesh}es may have different values for this property. * * @type {Boolean} */ get highlighted() { return this._highlighted; } /** * Sets if this Node and all child Nodes and {@link Mesh}es are selected. * * When {@link Node#isObject} and {@link Node#selected} are both ````true```` the Node will be * registered by {@link Node#id} in {@link Scene#selectedObjects}. * * @type {Boolean} */ set selected(selected) { selected = !!selected; this._selected = selected; for (let i = 0, len = this._children.length; i < len; i++) { this._children[i].selected = selected; } if (this._isObject) { this.scene._objectSelectedUpdated(this, selected); } } /** * Gets if this Node is selected. * * When {@link Node#isObject} and {@link Node#selected} are both ````true```` the Node will be * registered by {@link Node#id} in {@link Scene#selectedObjects}. * * Child Nodes and {@link Mesh}es may have different values for this property. * * @type {Boolean} */ get selected() { return this._selected; } /** * Sets if this Node and all child Nodes and {@link Mesh}es are edge-enhanced. * * @type {Boolean} */ set edges(edges) { edges = !!edges; this._edges = edges; for (let i = 0, len = this._children.length; i < len; i++) { this._children[i].edges = edges; } } /** * Gets if this Node's edges are enhanced. * * Child Nodes and {@link Mesh}es may have different values for this property. * * @type {Boolean} */ get edges() { return this._edges; } /** * Sets if this Node and all child Nodes and {@link Mesh}es are culled. * * @type {Boolean} */ set culled(culled) { culled = !!culled; this._culled = culled; for (let i = 0, len = this._children.length; i < len; i++) { this._children[i].culled = culled; } } /** * Gets if this Node is culled. * * @type {Boolean} */ get culled() { return this._culled; } /** * Sets if this Node and all child Nodes and {@link Mesh}es are clippable. * * Clipping is done by the {@link SectionPlane}s in {@link Scene#clips}. * * @type {Boolean} */ set clippable(clippable) { clippable = clippable !== false; this._clippable = clippable; for (let i = 0, len = this._children.length; i < len; i++) { this._children[i].clippable = clippable; } } /** * Gets if this Node is clippable. * * Clipping is done by the {@link SectionPlane}s in {@link Scene#clips}. * * Child Nodes and {@link Mesh}es may have different values for this property. * * @type {Boolean} */ get clippable() { return this._clippable; } /** * Sets if this Node and all child Nodes and {@link Mesh}es are included in boundary calculations. * * @type {Boolean} */ set collidable(collidable) { collidable = collidable !== false; this._collidable = collidable; for (let i = 0, len = this._children.length; i < len; i++) { this._children[i].collidable = collidable; } } /** * Gets if this Node is included in boundary calculations. * * Child Nodes and {@link Mesh}es may have different values for this property. * * @type {Boolean} */ get collidable() { return this._collidable; } /** * Sets if this Node and all child Nodes and {@link Mesh}es are pickable. * * Picking is done via calls to {@link Scene#pick}. * * @type {Boolean} */ set pickable(pickable) { pickable = pickable !== false; this._pickable = pickable; for (let i = 0, len = this._children.length; i < len; i++) { this._children[i].pickable = pickable; } } /** * Gets if to this Node is pickable. * * Picking is done via calls to {@link Scene#pick}. * * Child Nodes and {@link Mesh}es may have different values for this property. * * @type {Boolean} */ get pickable() { return this._pickable; } /** * Sets the RGB colorize color for this Node and all child Nodes and {@link Mesh}es}. * * Multiplies by rendered fragment colors. * * Each element of the color is in range ````[0..1]````. * * @type {Number[]} */ set colorize(rgb) { let colorize = this._colorize; if (!colorize) { colorize = this._colorize = new Float32Array(4); colorize[3] = 1.0; } if (rgb) { colorize[0] = rgb[0]; colorize[1] = rgb[1]; colorize[2] = rgb[2]; } else { colorize[0] = 1; colorize[1] = 1; colorize[2] = 1; } for (let i = 0, len = this._children.length; i < len; i++) { this._children[i].colorize = colorize; } if (this._isObject) { const colorized = (!!rgb); this.scene._objectColorizeUpdated(this, colorized); } } /** * Gets the RGB colorize color for this Node. * * Each element of the color is in range ````[0..1]````. * * Child Nodes and {@link Mesh}es may have different values for this property. * * @type {Number[]} */ get colorize() { return this._colorize.slice(0, 3); } /** * Sets the opacity factor for this Node and all child Nodes and {@link Mesh}es. * * This is a factor in range ````[0..1]```` which multiplies by the rendered fragment alphas. * * @type {Number} */ set opacity(opacity) { let colorize = this._colorize; if (!colorize) { colorize = this._colorize = new Float32Array(4); colorize[0] = 1; colorize[1] = 1; colorize[2] = 1; } colorize[3] = opacity !== null && opacity !== undefined ? opacity : 1.0; for (let i = 0, len = this._children.length; i < len; i++) { this._children[i].opacity = opacity; } if (this._isObject) { const opacityUpdated = (opacity !== null && opacity !== undefined); this.scene._objectOpacityUpdated(this, opacityUpdated); } } /** * Gets this Node's opacity factor. * * This is a factor in range ````[0..1]```` which multiplies by the rendered fragment alphas. * * Child Nodes and {@link Mesh}es may have different values for this property. * * @type {Number} */ get opacity() { return this._colorize[3]; } /** * Sets if this Node and all child Nodes and {@link Mesh}es cast shadows. * * @type {Boolean} */ set castsShadow(castsShadow) { castsShadow = !!castsShadow; this._castsShadow = castsShadow; for (let i = 0, len = this._children.length; i < len; i++) { this._children[i].castsShadow = castsShadow; } } /** * Gets if this Node casts shadows. * * Child Nodes and {@link Mesh}es may have different values for this property. * * @type {Boolean} */ get castsShadow() { return this._castsShadow; } /** * Sets if this Node and all child Nodes and {@link Mesh}es can have shadows cast upon them. * * @type {Boolean} */ set receivesShadow(receivesShadow) { receivesShadow = !!receivesShadow; this._receivesShadow = receivesShadow; for (let i = 0, len = this._children.length; i < len; i++) { this._children[i].receivesShadow = receivesShadow; } } /** * Whether or not to this Node can have shadows cast upon it. * * Child Nodes and {@link Mesh}es may have different values for this property. * * @type {Boolean} */ get receivesShadow() { return this._receivesShadow; } /** * Gets if this Node can have Scalable Ambient Obscurance (SAO) applied to it. * * SAO is configured by {@link SAO}. * * @type {Boolean} * @abstract */ get saoEnabled() { return false; // TODO: Support SAO on Nodes } /** * Sets the 3D World-space offset for this Node and all child Nodes and {@link Mesh}es}. * * The offset dynamically translates those components in World-space. * * Default value is ````[0, 0, 0]````. * * Note that child Nodes and {@link Mesh}es may subsequently be given different values for this property. * * @type {Number[]} */ set offset(offset) { if (offset) { this._offset[0] = offset[0]; this._offset[1] = offset[1]; this._offset[2] = offset[2]; } else { this._offset[0] = 0; this._offset[1] = 0; this._offset[2] = 0; } for (let i = 0, len = this._children.length; i < len; i++) { this._children[i].offset = this._offset; } if (this._isObject) { this.scene._objectOffsetUpdated(this, offset); } } /** * Gets the Node's 3D World-space offset. * * Default value is ````[0, 0, 0]````. * * Child Nodes and {@link Mesh}es may have different values for this property. * * @type {Number[]} */ get offset() { return this._offset; } //------------------------------------------------------------------------------------------------------------------ // Node members //------------------------------------------------------------------------------------------------------------------ /** * Returns true to indicate that this Component is a Node. * @type {Boolean} */ get isNode() { return true; } _setLocalMatrixDirty() { this._localMatrixDirty = true; this._setWorldMatrixDirty(); } _setWorldMatrixDirty() { this._worldMatrixDirty = true; for (let i = 0, len = this._children.length; i < len; i++) { this._children[i]._setWorldMatrixDirty(); } } _buildWorldMatrix() { const localMatrix = this.matrix; if (!this._parentNode) { for (let i = 0, len = localMatrix.length; i < len; i++) { this._worldMatrix[i] = localMatrix[i]; } } else { math.mulMat4(this._parentNode.worldMatrix, localMatrix, this._worldMatrix); } this._worldMatrixDirty = false; } _setSubtreeAABBsDirty(node) { node._aabbDirty = true; if (node._children) { for (let i = 0, len = node._children.length; i < len; i++) { this._setSubtreeAABBsDirty(node._children[i]); } } } _setAABBDirty() { this._setSubtreeAABBsDirty(this); if (this.collidable) { for (let node = this; node; node = node._parentNode) { node._aabbDirty = true; } } } _updateAABB() { this.scene._aabbDirty = true; if (!this._aabb) { this._aabb = math.AABB3(); } if (this._buildAABB) { this._buildAABB(this.worldMatrix, this._aabb); // Mesh or VBOSceneModel } else { // Node | Node | Model math.collapseAABB3(this._aabb); let node; for (let i = 0, len = this._children.length; i < len; i++) { node = this._children[i]; if (!node.collidable) { continue; } math.expandAABB3(this._aabb, node.aabb); } } this._aabbDirty = false; } /** * Adds a child Node or {@link Mesh}. * * The child must be a Node or {@link Mesh} in the same {@link Scene}. * * If the child already has a parent, will be removed from that parent first. * * Does nothing if already a child. * * @param {Node|Mesh|String} child Instance or ID of the child to add. * @param [inheritStates=false] Indicates if the child should inherit rendering states from this parent as it is added. Rendering state includes {@link Node#visible}, {@link Node#culled}, {@link Node#pickable}, {@link Node#clippable}, {@link Node#castsShadow}, {@link Node#receivesShadow}, {@link Node#selected}, {@link Node#highlighted}, {@link Node#colorize} and {@link Node#opacity}. * @returns {Node|Mesh} The child. */ addChild(child, inheritStates) { if (utils.isNumeric(child) || utils.isString(child)) { const nodeId = child; child = this.scene.component[nodeId]; if (!child) { this.warn("Component not found: " + utils.inQuotes(nodeId)); return; } if (!child.isNode && !child.isMesh) { this.error("Not a Node or Mesh: " + nodeId); return; } } else { if (!child.isNode && !child.isMesh) { this.error("Not a Node or Mesh: " + child.id); return; } if (child._parentNode) { if (child._parentNode.id === this.id) { this.warn("Already a child: " + child.id); return; } child._parentNode.removeChild(child); } } child.id; if (child.scene.id !== this.scene.id) { this.error("Child not in same Scene: " + child.id); return; } this._children.push(child); child._parentNode = this; if (!!inheritStates) { child.visible = this.visible; child.culled = this.culled; child.xrayed = this.xrayed; child.highlited = this.highlighted; child.selected = this.selected; child.edges = this.edges; child.clippable = this.clippable; child.pickable = this.pickable; child.collidable = this.collidable; child.castsShadow = this.castsShadow; child.receivesShadow = this.receivesShadow; child.colorize = this.colorize; child.opacity = this.opacity; child.offset = this.offset; } child._setWorldMatrixDirty(); child._setAABBDirty(); this._numTriangles += child.numTriangles; return child; } /** * Removes the given child Node or {@link Mesh}. * * @param {Node|Mesh} child Child to remove. */ removeChild(child) { for (let i = 0, len = this._children.length; i < len; i++) { if (this._children[i].id === child.id) { child._parentNode = null; this._children = this._children.splice(i, 1); child._setWorldMatrixDirty(); child._setAABBDirty(); this._setAABBDirty(); this._numTriangles -= child.numTriangles; return; } } } /** * Removes all child Nodes and {@link Mesh}es. */ removeChildren() { let child; for (let i = 0, len = this._children.length; i < len; i++) { child = this._children[i]; child._parentNode = null; child._setWorldMatrixDirty(); child._setAABBDirty(); this._numTriangles -= child.numTriangles; } this._children = []; this._setAABBDirty(); } /** * Number of child Nodes or {@link Mesh}es. * * @type {Number} */ get numChildren() { return this._children.length; } /** * Array of child Nodes or {@link Mesh}es. * * @type {Array} */ get children() { return this._children; } /** * The parent Node. * * The parent Node may also be set by passing the Node to the parent's {@link Node#addChild} method. * * @type {Node} */ set parent(node) { if (utils.isNumeric(node) || utils.isString(node)) { const nodeId = node; node = this.scene.components[nodeId]; if (!node) { this.warn("Node not found: " + utils.inQuotes(nodeId)); return; } if (!node.isNode) { this.error("Not a Node: " + node.id); return; } } if (node.scene.id !== this.scene.id) { this.error("Node not in same Scene: " + node.id); return; } if (this._parentNode && this._parentNode.id === node.id) { this.warn("Already a child of Node: " + node.id); return; } node.addChild(this); } /** * The parent Node. * * @type {Node} */ get parent() { return this._parentNode; } /** * Sets the Node's local translation. * * Default value is ````[0,0,0]````. * * @type {Number[]} */ set position(value) { this._position.set(value || [0, 0, 0]); this._setLocalMatrixDirty(); this._setAABBDirty(); this.glRedraw(); } /** * Gets the Node's local translation. * * Default value is ````[0,0,0]````. * * @type {Number[]} */ get position() { return this._position; } /** * Sets the Node's local rotation, as Euler angles given in degrees, for each of the X, Y and Z axis. * * Default value is ````[0,0,0]````. * * @type {Number[]} */ set rotation(value) { this._rotation.set(value || [0, 0, 0]); math.eulerToQuaternion(this._rotation, "XYZ", this._quaternion); this._setLocalMatrixDirty(); this._setAABBDirty(); this.glRedraw(); } /** * Gets the Node's local rotation, as Euler angles given in degrees, for each of the X, Y and Z axis. * * Default value is ````[0,0,0]````. * * @type {Number[]} */ get rotation() { return this._rotation; } /** * Sets the Node's local rotation quaternion. * * Default value is ````[0,0,0,1]````. * * @type {Number[]} */ set quaternion(value) { this._quaternion.set(value || [0, 0, 0, 1]); math.quaternionToEuler(this._quaternion, "XYZ", this._rotation); this._setLocalMatrixDirty(); this._setAABBDirty(); this.glRedraw(); } /** * Gets the Node's local rotation quaternion. * * Default value is ````[0,0,0,1]````. * * @type {Number[]} */ get quaternion() { return this._quaternion; } /** * Sets the Node's local scale. * * Default value is ````[1,1,1]````. * * @type {Number[]} */ set scale(value) { this._scale.set(value || [1, 1, 1]); this._setLocalMatrixDirty(); this._setAABBDirty(); this.glRedraw(); } /** * Gets the Node's local scale. * * Default value is ````[1,1,1]````. * * @type {Number[]} */ get scale() { return this._scale; } /** * Sets the Node's local modeling transform matrix. * * Default value is ````[1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]````. * * @type {Number[]} */ set matrix(value) { if (!this._localMatrix) { this._localMatrix = math.identityMat4(); } this._localMatrix.set(value || identityMat$2); math.decomposeMat4(this._localMatrix, this._position, this._quaternion, this._scale); this._localMatrixDirty = false; this._setWorldMatrixDirty(); this._setAABBDirty(); this.glRedraw(); } /** * Gets the Node's local modeling transform matrix. * * Default value is ````[1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]````. * * @type {Number[]} */ get matrix() { if (this._localMatrixDirty) { if (!this._localMatrix) { this._localMatrix = math.identityMat4(); } math.composeMat4(this._position, this._quaternion, this._scale, this._localMatrix); this._localMatrixDirty = false; } return this._localMatrix; } /** * Gets the Node's World matrix. * * @property worldMatrix * @type {Number[]} */ get worldMatrix() { if (this._worldMatrixDirty) { this._buildWorldMatrix(); } return this._worldMatrix; } /** * Rotates the Node about the given local axis by the given increment. * * @param {Number[]} axis Local axis about which to rotate. * @param {Number} angle Angle increment in degrees. */ rotate(axis, angle) { angleAxis$2[0] = axis[0]; angleAxis$2[1] = axis[1]; angleAxis$2[2] = axis[2]; angleAxis$2[3] = angle * math.DEGTORAD; math.angleAxisToQuaternion(angleAxis$2, q1$2); math.mulQuaternions(this.quaternion, q1$2, q2$2); this.quaternion = q2$2; this._setLocalMatrixDirty(); this._setAABBDirty(); this.glRedraw(); return this; } /** * Rotates the Node about the given World-space axis by the given increment. * * @param {Number[]} axis Local axis about which to rotate. * @param {Number} angle Angle increment in degrees. */ rotateOnWorldAxis(axis, angle) { angleAxis$2[0] = axis[0]; angleAxis$2[1] = axis[1]; angleAxis$2[2] = axis[2]; angleAxis$2[3] = angle * math.DEGTORAD; math.angleAxisToQuaternion(angleAxis$2, q1$2); math.mulQuaternions(q1$2, this.quaternion, q1$2); //this.quaternion.premultiply(q1); return this; } /** * Rotates the Node about the local X-axis by the given increment. * * @param {Number} angle Angle increment in degrees. */ rotateX(angle) { return this.rotate(xAxis$2, angle); } /** * Rotates the Node about the local Y-axis by the given increment. * * @param {Number} angle Angle increment in degrees. */ rotateY(angle) { return this.rotate(yAxis$2, angle); } /** * Rotates the Node about the local Z-axis by the given increment. * * @param {Number} angle Angle increment in degrees. */ rotateZ(angle) { return this.rotate(zAxis$2, angle); } /** * Translates the Node along local space vector by the given increment. * * @param {Number[]} axis Normalized local space 3D vector along which to translate. * @param {Number} distance Distance to translate along the vector. */ translate(axis, distance) { math.vec3ApplyQuaternion(this.quaternion, axis, veca$1); math.mulVec3Scalar(veca$1, distance, vecb$1); math.addVec3(this.position, vecb$1, this.position); this._setLocalMatrixDirty(); this._setAABBDirty(); this.glRedraw(); return this; } /** * Translates the Node along the local X-axis by the given increment. * * @param {Number} distance Distance to translate along the X-axis. */ translateX(distance) { return this.translate(xAxis$2, distance); } /** * Translates the Node along the local Y-axis by the given increment. * * @param {Number} distance Distance to translate along the Y-axis. */ translateY(distance) { return this.translate(yAxis$2, distance); } /** * Translates the Node along the local Z-axis by the given increment. * * @param {Number} distance Distance to translate along the Z-axis. */ translateZ(distance) { return this.translate(zAxis$2, distance); } //------------------------------------------------------------------------------------------------------------------ // Component members //------------------------------------------------------------------------------------------------------------------ /** @private */ get type() { return "Node"; } /** * Destroys this Node. */ destroy() { super.destroy(); if (this._parentNode) { this._parentNode.removeChild(this); } if (this._isObject) { this.scene._deregisterObject(this); if (this._visible) { this.scene._objectVisibilityUpdated(this, false, false); } if (this._xrayed) { this.scene._objectXRayedUpdated(this, false, false); } if (this._selected) { this.scene._objectSelectedUpdated(this, false, false); } if (this._highlighted) { this.scene._objectHighlightedUpdated(this, false, false); } this.scene._objectColorizeUpdated(this, false); this.scene._objectOpacityUpdated(this, false); if (this.offset.some((v) => v !== 0)) this.scene._objectOffsetUpdated(this, false); } if (this._isModel) { this.scene._deregisterModel(this); } if (this._children.length) { // Clone the _children before iterating, so our children don't mess us up when calling removeChild(). const tempChildList = this._children.splice(); let child; for (let i = 0, len = tempChildList.length; i < len; i++) { child = tempChildList[i]; child.destroy(); } } this._children = []; this._setAABBDirty(); this.scene._aabbDirty = true; } } const tempVec3a$M = math.vec3(); const tempVec4$1 = math.vec4(); /** * Given a view matrix and a relative-to-center (RTC) coordinate origin, returns a view matrix * to transform RTC coordinates to View-space. * * The returned view matrix is * * @private */ const createRTCViewMat = (function () { const tempMat = new Float64Array(16); const rtcCenterWorld = new Float64Array(4); const rtcCenterView = new Float64Array(4); return function (viewMat, rtcCenter, rtcViewMat) { rtcViewMat = rtcViewMat || tempMat; rtcCenterWorld[0] = rtcCenter[0]; rtcCenterWorld[1] = rtcCenter[1]; rtcCenterWorld[2] = rtcCenter[2]; rtcCenterWorld[3] = 1; math.transformVec4(viewMat, rtcCenterWorld, rtcCenterView); math.setMat4Translation(viewMat, rtcCenterView, rtcViewMat); return rtcViewMat.slice (); } }()); /** * Converts a World-space 3D position to RTC. * * Given a double-precision World-space position, returns a double-precision relative-to-center (RTC) center pos * and a single-precision offset fom that center. * @private * @param {Float64Array} worldPos The World-space position. * @param {Float64Array} rtcCenter Double-precision relative-to-center (RTC) center pos. * @param {Float32Array} rtcPos Single-precision offset fom that center. */ function worldToRTCPos(worldPos, rtcCenter, rtcPos) { const xHigh = Float32Array.from([worldPos[0]])[0]; const xLow = worldPos[0] - xHigh; const yHigh = Float32Array.from([worldPos[1]])[0]; const yLow = worldPos[1] - yHigh; const zHigh = Float32Array.from([worldPos[2]])[0]; const zLow = worldPos[2] - zHigh; rtcCenter[0] = xHigh; rtcCenter[1] = yHigh; rtcCenter[2] = zHigh; rtcPos[0] = xLow; rtcPos[1] = yLow; rtcPos[2] = zLow; } /** * Converts a flat array of double-precision positions to RTC positions, if necessary. * * Conversion is necessary if the coordinates have values larger than can be expressed at single-precision. When * that's the case, then this function will compute the RTC coordinates and RTC center and return true. Otherwise * this function does nothing and returns false. * * When computing the RTC position, this function uses a modulus operation to ensure that, whenever possible, * identical RTC centers are reused for different positions arrays. * * @private * @param {Float64Array} worldPositions Flat array of World-space 3D positions. * @param {Float64Array} rtcPositions Outputs the computed flat array of 3D RTC positions. * @param {Float64Array} rtcCenter Outputs the computed double-precision relative-to-center (RTC) center pos. * @param {Number} [cellSize=10000000] The size of each coordinate cell within the RTC coordinate system. * @returns {Boolean} ````True```` if the positions actually needed conversion to RTC, else ````false````. When * ````false````, we can safely ignore the data returned in ````rtcPositions```` and ````rtcCenter````, * since ````rtcCenter```` will equal ````[0,0,0]````, and ````rtcPositions```` will contain identical values to ````positions````. */ function worldToRTCPositions(worldPositions, rtcPositions, rtcCenter, cellSize = 1000) { const center = math.getPositionsCenter(worldPositions, tempVec3a$M); const rtcCenterX = Math.round(center[0] / cellSize) * cellSize; const rtcCenterY = Math.round(center[1] / cellSize) * cellSize; const rtcCenterZ = Math.round(center[2] / cellSize) * cellSize; rtcCenter[0] = rtcCenterX; rtcCenter[1] = rtcCenterY; rtcCenter[2] = rtcCenterZ; const rtcNeeded = (rtcCenter[0] !== 0 || rtcCenter[1] !== 0 || rtcCenter[2] !== 0); if (rtcNeeded) { for (let i = 0, len = worldPositions.length; i < len; i += 3) { rtcPositions[i + 0] = worldPositions[i + 0] - rtcCenterX; rtcPositions[i + 1] = worldPositions[i + 1] - rtcCenterY; rtcPositions[i + 2] = worldPositions[i + 2] - rtcCenterZ; } } return rtcNeeded; } /** * Converts an RTC 3D position to World-space. * * @private * @param {Float64Array} rtcCenter Double-precision relative-to-center (RTC) center pos. * @param {Float32Array} rtcPos Single-precision offset fom that center. * @param {Float64Array} worldPos The World-space position. */ function rtcToWorldPos(rtcCenter, rtcPos, worldPos) { worldPos[0] = rtcCenter[0] + rtcPos[0]; worldPos[1] = rtcCenter[1] + rtcPos[1]; worldPos[2] = rtcCenter[2] + rtcPos[2]; return worldPos; } /** * Given a 3D plane defined by distance from origin and direction, and an RTC center position, * return a plane position that is relative to the RTC center. * * @private * @param dist * @param dir * @param rtcCenter * @param rtcPlanePos * @returns {*} */ function getPlaneRTCPos(dist, dir, rtcOrigin, rtcPlanePos, originMatrix) { const rtcCenter = (originMatrix ? (tempVec4$1.set(rtcOrigin, 0), tempVec4$1[3] = 1, math.mulMat4v4(originMatrix, tempVec4$1, tempVec4$1)) : rtcOrigin); const rtcCenterToPlaneDist = math.dotVec3(dir, rtcCenter) + dist; const dirNormalized = math.normalizeVec3(dir, tempVec3a$M); math.mulVec3Scalar(dirNormalized, -rtcCenterToPlaneDist, rtcPlanePos); return rtcPlanePos; } /** * Texture wrapping mode in which the texture repeats to infinity. */ const RepeatWrapping = 1000; /** * Texture wrapping mode in which the last pixel of the texture stretches to the edge of the mesh. */ const ClampToEdgeWrapping = 1001; /** * Texture wrapping mode in which the texture repeats to infinity, mirroring on each repeat. */ const MirroredRepeatWrapping = 1002; /** * Texture magnification and minification filter that returns the nearest texel to the given sample coordinates. */ const NearestFilter = 1003; /** * Texture minification filter that chooses the mipmap that most closely matches the size of the pixel being textured and returns the nearest texel to the given sample coordinates. */ const NearestMipMapNearestFilter = 1004; /** * Texture minification filter that chooses the mipmap that most closely matches the size of the pixel being textured * and returns the nearest texel to the given sample coordinates. */ const NearestMipmapNearestFilter = 1004; /** * Texture minification filter that chooses two mipmaps that most closely match the size of the pixel being textured * and returns the nearest texel to the center of the pixel at the given sample coordinates. */ const NearestMipmapLinearFilter = 1005; /** * Texture minification filter that chooses two mipmaps that most closely match the size of the pixel being textured * and returns the nearest texel to the center of the pixel at the given sample coordinates. */ const NearestMipMapLinearFilter = 1005; /** * Texture magnification and minification filter that returns the weighted average of the four nearest texels to the given sample coordinates. */ const LinearFilter = 1006; /** * Texture minification filter that chooses the mipmap that most closely matches the size of the pixel being textured and * returns the weighted average of the four nearest texels to the given sample coordinates. */ const LinearMipmapNearestFilter = 1007; /** * Texture minification filter that chooses the mipmap that most closely matches the size of the pixel being textured and * returns the weighted average of the four nearest texels to the given sample coordinates. */ const LinearMipMapNearestFilter = 1007; /** * Texture minification filter that chooses two mipmaps that most closely match the size of the pixel being textured, * finds within each mipmap the weighted average of the nearest texel to the center of the pixel, then returns the * weighted average of those two values. */ const LinearMipmapLinearFilter = 1008; /** * Texture minification filter that chooses two mipmaps that most closely match the size of the pixel being textured, * finds within each mipmap the weighted average of the nearest texel to the center of the pixel, then returns the * weighted average of those two values. */ const LinearMipMapLinearFilter = 1008; /** * Unsigned 8-bit integer type. */ const UnsignedByteType = 1009; /** * Signed 8-bit integer type. */ const ByteType = 1010; /** * Signed 16-bit integer type. */ const ShortType = 1011; /** * Unsigned 16-bit integer type. */ const UnsignedShortType = 1012; /** * Signed 32-bit integer type. */ const IntType = 1013; /** * Unsigned 32-bit integer type. */ const UnsignedIntType = 1014; /** * Signed 32-bit floating-point type. */ const FloatType = 1015; /** * Signed 16-bit half-precision floating-point type. */ const HalfFloatType = 1016; /** * Texture packing mode in which each ````RGBA```` channel is packed into 4 bits, for a combined total of 16 bits. */ const UnsignedShort4444Type = 1017; /** * Texture packing mode in which the ````RGB```` channels are each packed into 5 bits, and the ````A```` channel is packed into 1 bit, for a combined total of 16 bits. */ const UnsignedShort5551Type = 1018; /** * Unsigned integer type for 24-bit depth texture data. */ const UnsignedInt248Type = 1020; /** * Texture sampling mode that discards the ````RGBA```` components and just reads the ````A```` component. */ const AlphaFormat = 1021; /** * Texture sampling mode that discards the ````A```` component and reads the ````RGB```` components. */ const RGBFormat = 1022; /** * Texture sampling mode that reads the ````RGBA```` components. */ const RGBAFormat = 1023; /** * Texture sampling mode that reads each ````RGB```` texture component as a luminance value, converted to a float and clamped * to ````[0,1]````, while always reading the ````A```` channel as ````1.0````. */ const LuminanceFormat = 1024; /** * Texture sampling mode that reads each of the ````RGBA```` texture components as a luminance/alpha value, converted to a float and clamped to ````[0,1]````. */ const LuminanceAlphaFormat = 1025; /** * Texture sampling mode that reads each element as a single depth value, converts it to a float and clamps to ````[0,1]````. */ const DepthFormat = 1026; /** * Texture sampling mode that */ const DepthStencilFormat = 1027; /** * Texture sampling mode that discards the ````GBA```` components and just reads the ````R```` component. */ const RedFormat = 1028; /** * Texture sampling mode that discards the ````GBA```` components and just reads the ````R```` component, as an integer instead of as a float. */ const RedIntegerFormat = 1029; /** * Texture sampling mode that discards the ````A```` and ````B```` components and just reads the ````R```` and ````G```` components. */ const RGFormat = 1030; /** * Texture sampling mode that discards the ````A```` and ````B```` components and just reads the ````R```` and ````G```` components, as integers instead of floats. */ const RGIntegerFormat = 1031; /** * Texture sampling mode that reads the ````RGBA```` components as integers instead of floats. */ const RGBAIntegerFormat = 1033; /** * Texture format mode in which the texture is formatted as a DXT1 compressed ````RGB```` image. */ const RGB_S3TC_DXT1_Format = 33776; /** * Texture format mode in which the texture is formatted as a DXT1 compressed ````RGBA```` image. */ const RGBA_S3TC_DXT1_Format = 33777; /** * Texture format mode in which the texture is formatted as a DXT3 compressed ````RGBA```` image. */ const RGBA_S3TC_DXT3_Format = 33778; /** * Texture format mode in which the texture is formatted as a DXT5 compressed ````RGBA```` image. */ const RGBA_S3TC_DXT5_Format = 33779; /** * Texture format mode in which the texture is formatted as a PVRTC compressed * image, with ````RGB```` compression in 4-bit mode and one block for each 4×4 pixels. */ const RGB_PVRTC_4BPPV1_Format = 35840; /** * Texture format mode in which the texture is formatted as a PVRTC compressed * image, with ````RGB```` compression in 2-bit mode and one block for each 8×4 pixels. */ const RGB_PVRTC_2BPPV1_Format = 35841; /** * Texture format mode in which the texture is formatted as a PVRTC compressed * image, with ````RGBA```` compression in 4-bit mode and one block for each 4×4 pixels. */ const RGBA_PVRTC_4BPPV1_Format = 35842; /** * Texture format mode in which the texture is formatted as a PVRTC compressed * image, with ````RGBA```` compression in 2-bit mode and one block for each 8×4 pixels. */ const RGBA_PVRTC_2BPPV1_Format = 35843; /** * Texture format mode in which the texture is formatted as an ETC1 compressed * ````RGB```` image. */ const RGB_ETC1_Format = 36196; /** * Texture format mode in which the texture is formatted as an ETC2 compressed * ````RGB```` image. */ const RGB_ETC2_Format = 37492; /** * Texture format mode in which the texture is formatted as an ETC2 compressed * ````RGBA```` image. */ const RGBA_ETC2_EAC_Format = 37496; /** * Texture format mode in which the texture is formatted as an ATSC compressed * ````RGBA```` image. */ const RGBA_ASTC_4x4_Format = 37808; /** * Texture format mode in which the texture is formatted as an ATSC compressed * ````RGBA```` image. */ const RGBA_ASTC_5x4_Format = 37809; /** * Texture format mode in which the texture is formatted as an ATSC compressed * ````RGBA```` image. */ const RGBA_ASTC_5x5_Format = 37810; /** * Texture format mode in which the texture is formatted as an ATSC compressed * ````RGBA```` image. */ const RGBA_ASTC_6x5_Format = 37811; /** * Texture format mode in which the texture is formatted as an ATSC compressed * ````RGBA```` image. */ const RGBA_ASTC_6x6_Format = 37812; /** * Texture format mode in which the texture is formatted as an ATSC compressed * ````RGBA```` image. */ const RGBA_ASTC_8x5_Format = 37813; /** * Texture format mode in which the texture is formatted as an ATSC compressed * ````RGBA```` image. */ const RGBA_ASTC_8x6_Format = 37814; /** * Texture format mode in which the texture is formatted as an ATSC compressed * ````RGBA```` image. */ const RGBA_ASTC_8x8_Format = 37815; /** * Texture format mode in which the texture is formatted as an ATSC compressed * ````RGBA```` image. */ const RGBA_ASTC_10x5_Format = 37816; /** * Texture format mode in which the texture is formatted as an ATSC compressed * ````RGBA```` image. */ const RGBA_ASTC_10x6_Format = 37817; /** * Texture format mode in which the texture is formatted as an ATSC compressed * ````RGBA```` image. */ const RGBA_ASTC_10x8_Format = 37818; /** * Texture format mode in which the texture is formatted as an ATSC compressed * ````RGBA```` image. */ const RGBA_ASTC_10x10_Format = 37819; /** * Texture format mode in which the texture is formatted as an ATSC compressed * ````RGBA```` image. */ const RGBA_ASTC_12x10_Format = 37820; /** * Texture format mode in which the texture is formatted as an ATSC compressed * ````RGBA```` image. */ const RGBA_ASTC_12x12_Format = 37821; /** * Texture format mode in which the texture is formatted as an BPTC compressed * ````RGBA```` image. */ const RGBA_BPTC_Format = 36492; /** * Texture encoding mode in which the texture image is in linear color space. */ const LinearEncoding = 3000; /** * Texture encoding mode in which the texture image is in sRGB color space. */ const sRGBEncoding = 3001; /** * Media type for GIF images. */ const GIFMediaType = 10000; /** * Media type for JPEG images. */ const JPEGMediaType = 10001; /** * Media type for PNG images. */ const PNGMediaType = 10002; /** * Media type for compressed texture data. */ const CompressedMediaType = 10003; /** * @private */ const DrawShaderSource = function (mesh) { if (mesh._material._state.type === "LambertMaterial") { this.vertex = buildVertexLambert(mesh); this.fragment = buildFragmentLambert(mesh); } else { this.vertex = buildVertexDraw(mesh); this.fragment = buildFragmentDraw(mesh); } }; const TEXTURE_DECODE_FUNCS$1 = {}; TEXTURE_DECODE_FUNCS$1[LinearEncoding] = "linearToLinear"; TEXTURE_DECODE_FUNCS$1[sRGBEncoding] = "sRGBToLinear"; function getReceivesShadow(mesh) { if (!mesh.receivesShadow) { return false; } const lights = mesh.scene._lightsState.lights; if (!lights || lights.length === 0) { return false; } for (let i = 0, len = lights.length; i < len; i++) { if (lights[i].castsShadow) { return true; } } return false; } function hasTextures(mesh) { if (!mesh._geometry._state.uvBuf) { return false; } const material = mesh._material; return !!(material._ambientMap || material._occlusionMap || material._baseColorMap || material._diffuseMap || material._alphaMap || material._specularMap || material._glossinessMap || material._specularGlossinessMap || material._emissiveMap || material._metallicMap || material._roughnessMap || material._metallicRoughnessMap || material._reflectivityMap || material._normalMap); } function hasNormals$1(mesh) { const primitive = mesh._geometry._state.primitiveName; if ((mesh._geometry._state.autoVertexNormals || mesh._geometry._state.normalsBuf) && (primitive === "triangles" || primitive === "triangle-strip" || primitive === "triangle-fan")) { return true; } return false; } function buildVertexLambert(mesh) { const scene = mesh.scene; const sectionPlanesState = mesh.scene._sectionPlanesState; const lightsState = mesh.scene._lightsState; const geometryState = mesh._geometry._state; const billboard = mesh._state.billboard; const stationary = mesh._state.stationary; const clipping = sectionPlanesState.getNumAllocatedSectionPlanes() > 0; const quantizedGeometry = !!geometryState.compressGeometry; const src = []; src.push("#version 300 es"); src.push("// Lambertian drawing vertex shader"); src.push("in vec3 position;"); src.push("uniform mat4 modelMatrix;"); src.push("uniform mat4 viewMatrix;"); src.push("uniform mat4 projMatrix;"); src.push("uniform vec4 colorize;"); src.push("uniform vec3 offset;"); if (quantizedGeometry) { src.push("uniform mat4 positionsDecodeMatrix;"); } if (scene.logarithmicDepthBufferEnabled) { src.push("uniform float logDepthBufFC;"); src.push("out float vFragDepth;"); src.push("bool isPerspectiveMatrix(mat4 m) {"); src.push(" return (m[2][3] == - 1.0);"); src.push("}"); src.push("out float isPerspective;"); } if (clipping) { src.push("out vec4 vWorldPosition;"); } src.push("uniform vec4 lightAmbient;"); src.push("uniform vec4 materialColor;"); src.push("uniform vec3 materialEmissive;"); if (geometryState.normalsBuf) { src.push("in vec3 normal;"); src.push("uniform mat4 modelNormalMatrix;"); src.push("uniform mat4 viewNormalMatrix;"); for (let i = 0, len = lightsState.lights.length; i < len; i++) { const light = lightsState.lights[i]; if (light.type === "ambient") { continue; } src.push("uniform vec4 lightColor" + i + ";"); if (light.type === "dir") { src.push("uniform vec3 lightDir" + i + ";"); } if (light.type === "point") { src.push("uniform vec3 lightPos" + i + ";"); } if (light.type === "spot") { src.push("uniform vec3 lightPos" + i + ";"); src.push("uniform vec3 lightDir" + i + ";"); } } if (quantizedGeometry) { src.push("vec3 octDecode(vec2 oct) {"); src.push(" vec3 v = vec3(oct.xy, 1.0 - abs(oct.x) - abs(oct.y));"); src.push(" if (v.z < 0.0) {"); src.push(" v.xy = (1.0 - abs(v.yx)) * vec2(v.x >= 0.0 ? 1.0 : -1.0, v.y >= 0.0 ? 1.0 : -1.0);"); src.push(" }"); src.push(" return normalize(v);"); src.push("}"); } } src.push("out vec4 vColor;"); if (geometryState.primitiveName === "points") { src.push("uniform float pointSize;"); } if (billboard === "spherical" || billboard === "cylindrical") { src.push("void billboard(inout mat4 mat) {"); src.push(" mat[0][0] = 1.0;"); src.push(" mat[0][1] = 0.0;"); src.push(" mat[0][2] = 0.0;"); if (billboard === "spherical") { src.push(" mat[1][0] = 0.0;"); src.push(" mat[1][1] = 1.0;"); src.push(" mat[1][2] = 0.0;"); } src.push(" mat[2][0] = 0.0;"); src.push(" mat[2][1] = 0.0;"); src.push(" mat[2][2] =1.0;"); src.push("}"); } src.push("void main(void) {"); src.push("vec4 localPosition = vec4(position, 1.0); "); src.push("vec4 worldPosition;"); if (quantizedGeometry) { src.push("localPosition = positionsDecodeMatrix * localPosition;"); } if (geometryState.normalsBuf) { if (quantizedGeometry) { src.push("vec4 localNormal = vec4(octDecode(normal.xy), 0.0); "); } else { src.push("vec4 localNormal = vec4(normal, 0.0); "); } src.push("mat4 modelNormalMatrix2 = modelNormalMatrix;"); src.push("mat4 viewNormalMatrix2 = viewNormalMatrix;"); } src.push("mat4 viewMatrix2 = viewMatrix;"); src.push("mat4 modelMatrix2 = modelMatrix;"); if (stationary) { src.push("viewMatrix2[3][0] = viewMatrix2[3][1] = viewMatrix2[3][2] = 0.0;"); } if (billboard === "spherical" || billboard === "cylindrical") { src.push("mat4 modelViewMatrix = viewMatrix2 * modelMatrix2;"); src.push("billboard(modelMatrix2);"); src.push("billboard(viewMatrix2);"); src.push("billboard(modelViewMatrix);"); if (geometryState.normalsBuf) { src.push("mat4 modelViewNormalMatrix = viewNormalMatrix2 * modelNormalMatrix2;"); src.push("billboard(modelNormalMatrix2);"); src.push("billboard(viewNormalMatrix2);"); src.push("billboard(modelViewNormalMatrix);"); } src.push("worldPosition = modelMatrix2 * localPosition;"); src.push("worldPosition.xyz = worldPosition.xyz + offset;"); src.push("vec4 viewPosition = modelViewMatrix * localPosition;"); } else { src.push("worldPosition = modelMatrix2 * localPosition;"); src.push("worldPosition.xyz = worldPosition.xyz + offset;"); src.push("vec4 viewPosition = viewMatrix2 * worldPosition; "); } if (geometryState.normalsBuf) { src.push("vec3 viewNormal = normalize((viewNormalMatrix2 * modelNormalMatrix2 * localNormal).xyz);"); } src.push("vec3 reflectedColor = vec3(0.0, 0.0, 0.0);"); src.push("vec3 viewLightDir = vec3(0.0, 0.0, -1.0);"); src.push("float lambertian = 1.0;"); if (geometryState.normalsBuf) { for (let i = 0, len = lightsState.lights.length; i < len; i++) { const light = lightsState.lights[i]; if (light.type === "ambient") { continue; } if (light.type === "dir") { if (light.space === "view") { src.push("viewLightDir = normalize(lightDir" + i + ");"); } else { src.push("viewLightDir = normalize((viewMatrix2 * vec4(lightDir" + i + ", 0.0)).xyz);"); } } else if (light.type === "point") { if (light.space === "view") { src.push("viewLightDir = -normalize(lightPos" + i + " - viewPosition.xyz);"); } else { src.push("viewLightDir = -normalize((viewMatrix2 * vec4(lightPos" + i + ", 0.0)).xyz);"); } } else if (light.type === "spot") { if (light.space === "view") { src.push("viewLightDir = normalize(lightDir" + i + ");"); } else { src.push("viewLightDir = normalize((viewMatrix2 * vec4(lightDir" + i + ", 0.0)).xyz);"); } } else { continue; } src.push("lambertian = max(dot(-viewNormal, viewLightDir), 0.0);"); src.push("reflectedColor += lambertian * (lightColor" + i + ".rgb * lightColor" + i + ".a);"); } } src.push("vColor = vec4((lightAmbient.rgb * lightAmbient.a * materialColor.rgb) + materialEmissive.rgb + (reflectedColor * materialColor.rgb), materialColor.a) * colorize;"); // TODO: How to have ambient bright enough for canvas BG but not too bright for scene? if (clipping) { src.push("vWorldPosition = worldPosition;"); } if (geometryState.primitiveName === "points") { src.push("gl_PointSize = pointSize;"); } src.push("vec4 clipPos = projMatrix * viewPosition;"); if (scene.logarithmicDepthBufferEnabled) { src.push("vFragDepth = 1.0 + clipPos.w;"); src.push("isPerspective = float (isPerspectiveMatrix(projMatrix));"); } src.push("gl_Position = clipPos;"); src.push("}"); return src; } function buildFragmentLambert(mesh) { const scene = mesh.scene; const sectionPlanesState = scene._sectionPlanesState; mesh._material._state; const geometryState = mesh._geometry._state; const clipping = sectionPlanesState.getNumAllocatedSectionPlanes() > 0; const gammaOutput = scene.gammaOutput; // If set, then it expects that all textures and colors need to be outputted in premultiplied gamma. Default is false. const src = []; src.push("#version 300 es"); src.push("// Lambertian drawing fragment shader"); src.push("#ifdef GL_FRAGMENT_PRECISION_HIGH"); src.push("precision highp float;"); src.push("precision highp int;"); src.push("#else"); src.push("precision mediump float;"); src.push("precision mediump int;"); src.push("#endif"); if (scene.logarithmicDepthBufferEnabled) { src.push("in float isPerspective;"); src.push("uniform float logDepthBufFC;"); src.push("in float vFragDepth;"); } if (clipping) { src.push("in vec4 vWorldPosition;"); src.push("uniform bool clippable;"); for (let i = 0, len = sectionPlanesState.getNumAllocatedSectionPlanes(); i < len; i++) { src.push("uniform bool sectionPlaneActive" + i + ";"); src.push("uniform vec3 sectionPlanePos" + i + ";"); src.push("uniform vec3 sectionPlaneDir" + i + ";"); } } src.push("in vec4 vColor;"); if (gammaOutput) { src.push("uniform float gammaFactor;"); src.push(" vec4 linearToGamma( in vec4 value, in float gammaFactor ) {"); src.push(" return vec4( pow( value.xyz, vec3( 1.0 / gammaFactor ) ), value.w );"); src.push("}"); } src.push("out vec4 outColor;"); src.push("void main(void) {"); if (clipping) { src.push("if (clippable) {"); src.push(" float dist = 0.0;"); for (let i = 0, len = sectionPlanesState.getNumAllocatedSectionPlanes(); i < len; i++) { src.push("if (sectionPlaneActive" + i + ") {"); src.push(" dist += clamp(dot(-sectionPlaneDir" + i + ".xyz, vWorldPosition.xyz - sectionPlanePos" + i + ".xyz), 0.0, 1000.0);"); src.push("}"); } src.push(" if (dist > 0.0) { discard; }"); src.push("}"); } if (geometryState.primitiveName === "points") { src.push("vec2 cxy = 2.0 * gl_PointCoord - 1.0;"); src.push("float r = dot(cxy, cxy);"); src.push("if (r > 1.0) {"); src.push(" discard;"); src.push("}"); } if (scene.logarithmicDepthBufferEnabled) { src.push("gl_FragDepth = isPerspective == 0.0 ? gl_FragCoord.z : log2( vFragDepth ) * logDepthBufFC * 0.5;"); } if (gammaOutput) { src.push("outColor = linearToGamma(vColor, gammaFactor);"); } else { src.push("outColor = vColor;"); } src.push("}"); return src; } function buildVertexDraw(mesh) { const scene = mesh.scene; mesh._material; const meshState = mesh._state; const sectionPlanesState = scene._sectionPlanesState; const geometryState = mesh._geometry._state; const lightsState = scene._lightsState; let light; const billboard = meshState.billboard; const background = meshState.background; const stationary = meshState.stationary; const texturing = hasTextures(mesh); const normals = hasNormals$1(mesh); const clipping = sectionPlanesState.getNumAllocatedSectionPlanes() > 0; const receivesShadow = getReceivesShadow(mesh); const quantizedGeometry = !!geometryState.compressGeometry; const src = []; src.push("#version 300 es"); src.push("// Drawing vertex shader"); src.push("in vec3 position;"); if (quantizedGeometry) { src.push("uniform mat4 positionsDecodeMatrix;"); } src.push("uniform mat4 modelMatrix;"); src.push("uniform mat4 viewMatrix;"); src.push("uniform mat4 projMatrix;"); src.push("out vec3 vViewPosition;"); src.push("uniform vec3 offset;"); if (clipping) { src.push("out vec4 vWorldPosition;"); } if (scene.logarithmicDepthBufferEnabled) { src.push("uniform float logDepthBufFC;"); src.push("out float vFragDepth;"); src.push("bool isPerspectiveMatrix(mat4 m) {"); src.push(" return (m[2][3] == - 1.0);"); src.push("}"); src.push("out float isPerspective;"); } if (lightsState.lightMaps.length > 0) { src.push("out vec3 vWorldNormal;"); } if (normals) { src.push("in vec3 normal;"); src.push("uniform mat4 modelNormalMatrix;"); src.push("uniform mat4 viewNormalMatrix;"); src.push("out vec3 vViewNormal;"); for (let i = 0, len = lightsState.lights.length; i < len; i++) { light = lightsState.lights[i]; if (light.type === "ambient") { continue; } if (light.type === "dir") { src.push("uniform vec3 lightDir" + i + ";"); } if (light.type === "point") { src.push("uniform vec3 lightPos" + i + ";"); } if (light.type === "spot") { src.push("uniform vec3 lightPos" + i + ";"); src.push("uniform vec3 lightDir" + i + ";"); } if (!(light.type === "dir" && light.space === "view")) { src.push("out vec4 vViewLightReverseDirAndDist" + i + ";"); } } if (quantizedGeometry) { src.push("vec3 octDecode(vec2 oct) {"); src.push(" vec3 v = vec3(oct.xy, 1.0 - abs(oct.x) - abs(oct.y));"); src.push(" if (v.z < 0.0) {"); src.push(" v.xy = (1.0 - abs(v.yx)) * vec2(v.x >= 0.0 ? 1.0 : -1.0, v.y >= 0.0 ? 1.0 : -1.0);"); src.push(" }"); src.push(" return normalize(v);"); src.push("}"); } } if (texturing) { src.push("in vec2 uv;"); src.push("out vec2 vUV;"); if (quantizedGeometry) { src.push("uniform mat3 uvDecodeMatrix;"); } } if (geometryState.colors) { src.push("in vec4 color;"); src.push("out vec4 vColor;"); } if (geometryState.primitiveName === "points") { src.push("uniform float pointSize;"); } if (billboard === "spherical" || billboard === "cylindrical") { src.push("void billboard(inout mat4 mat) {"); src.push(" mat[0][0] = 1.0;"); src.push(" mat[0][1] = 0.0;"); src.push(" mat[0][2] = 0.0;"); if (billboard === "spherical") { src.push(" mat[1][0] = 0.0;"); src.push(" mat[1][1] = 1.0;"); src.push(" mat[1][2] = 0.0;"); } src.push(" mat[2][0] = 0.0;"); src.push(" mat[2][1] = 0.0;"); src.push(" mat[2][2] =1.0;"); src.push("}"); } if (receivesShadow) { src.push("const mat4 texUnitConverter = mat4(0.5, 0.0, 0.0, 0.0, 0.0, 0.5, 0.0, 0.0, 0.0, 0.0, 0.5, 0.0, 0.5, 0.5, 0.5, 1.0);"); for (let i = 0, len = lightsState.lights.length; i < len; i++) { // Light sources if (lightsState.lights[i].castsShadow) { src.push("uniform mat4 shadowViewMatrix" + i + ";"); src.push("uniform mat4 shadowProjMatrix" + i + ";"); src.push("out vec4 vShadowPosFromLight" + i + ";"); } } } src.push("void main(void) {"); src.push("vec4 localPosition = vec4(position, 1.0); "); src.push("vec4 worldPosition;"); if (quantizedGeometry) { src.push("localPosition = positionsDecodeMatrix * localPosition;"); } if (normals) { if (quantizedGeometry) { src.push("vec4 localNormal = vec4(octDecode(normal.xy), 0.0); "); } else { src.push("vec4 localNormal = vec4(normal, 0.0); "); } src.push("mat4 modelNormalMatrix2 = modelNormalMatrix;"); src.push("mat4 viewNormalMatrix2 = viewNormalMatrix;"); } src.push("mat4 viewMatrix2 = viewMatrix;"); src.push("mat4 modelMatrix2 = modelMatrix;"); if (stationary) { src.push("viewMatrix2[3][0] = viewMatrix2[3][1] = viewMatrix2[3][2] = 0.0;"); } else if (background) { src.push("viewMatrix2[3] = vec4(0.0, 0.0, 0.0 ,1.0);"); } if (billboard === "spherical" || billboard === "cylindrical") { src.push("mat4 modelViewMatrix = viewMatrix2 * modelMatrix2;"); src.push("billboard(modelMatrix2);"); src.push("billboard(viewMatrix2);"); src.push("billboard(modelViewMatrix);"); if (normals) { src.push("mat4 modelViewNormalMatrix = viewNormalMatrix2 * modelNormalMatrix2;"); src.push("billboard(modelNormalMatrix2);"); src.push("billboard(viewNormalMatrix2);"); src.push("billboard(modelViewNormalMatrix);"); } src.push("worldPosition = modelMatrix2 * localPosition;"); src.push("worldPosition.xyz = worldPosition.xyz + offset;"); src.push("vec4 viewPosition = modelViewMatrix * localPosition;"); } else { src.push("worldPosition = modelMatrix2 * localPosition;"); src.push("worldPosition.xyz = worldPosition.xyz + offset;"); src.push("vec4 viewPosition = viewMatrix2 * worldPosition; "); } if (normals) { src.push("vec3 worldNormal = (modelNormalMatrix2 * localNormal).xyz; "); if (lightsState.lightMaps.length > 0) { src.push("vWorldNormal = worldNormal;"); } src.push("vViewNormal = normalize((viewNormalMatrix2 * vec4(worldNormal, 1.0)).xyz);"); src.push("vec3 tmpVec3;"); src.push("float lightDist;"); for (let i = 0, len = lightsState.lights.length; i < len; i++) { // Lights light = lightsState.lights[i]; if (light.type === "ambient") { continue; } if (light.type === "dir") { if (light.space === "world") { src.push("tmpVec3 = vec3(viewMatrix2 * vec4(lightDir" + i + ", 0.0) ).xyz;"); src.push("vViewLightReverseDirAndDist" + i + " = vec4(-tmpVec3, 0.0);"); } } if (light.type === "point") { if (light.space === "world") { src.push("tmpVec3 = (viewMatrix2 * vec4(lightPos" + i + ", 1.0)).xyz - viewPosition.xyz;"); src.push("lightDist = abs(length(tmpVec3));"); } else { src.push("tmpVec3 = lightPos" + i + ".xyz - viewPosition.xyz;"); src.push("lightDist = abs(length(tmpVec3));"); } src.push("vViewLightReverseDirAndDist" + i + " = vec4(tmpVec3, lightDist);"); } } } if (texturing) { if (quantizedGeometry) { src.push("vUV = (uvDecodeMatrix * vec3(uv, 1.0)).xy;"); } else { src.push("vUV = uv;"); } } if (geometryState.colors) { src.push("vColor = color;"); } if (geometryState.primitiveName === "points") { src.push("gl_PointSize = pointSize;"); } if (clipping) { src.push("vWorldPosition = worldPosition;"); } src.push(" vViewPosition = viewPosition.xyz;"); src.push("vec4 clipPos = projMatrix * viewPosition;"); if (scene.logarithmicDepthBufferEnabled) { src.push("vFragDepth = 1.0 + clipPos.w;"); src.push("isPerspective = float (isPerspectiveMatrix(projMatrix));"); } if (background) { src.push("clipPos.z = clipPos.w;"); } src.push("gl_Position = clipPos;"); if (receivesShadow) { src.push("const mat4 texUnitConverter = mat4(0.5, 0.0, 0.0, 0.0, 0.0, 0.5, 0.0, 0.0, 0.0, 0.0, 0.5, 0.0, 0.5, 0.5, 0.5, 1.0);"); src.push("vec4 tempx; "); for (let i = 0, len = lightsState.lights.length; i < len; i++) { // Light sources if (lightsState.lights[i].castsShadow) { src.push("vShadowPosFromLight" + i + " = texUnitConverter * shadowProjMatrix" + i + " * (shadowViewMatrix" + i + " * worldPosition); "); } } } src.push("}"); return src; } function buildFragmentDraw(mesh) { const scene = mesh.scene; scene.canvas.gl; const material = mesh._material; const geometryState = mesh._geometry._state; const sectionPlanesState = mesh.scene._sectionPlanesState; const lightsState = mesh.scene._lightsState; const materialState = mesh._material._state; const clipping = sectionPlanesState.getNumAllocatedSectionPlanes() > 0; const normals = hasNormals$1(mesh); const uvs = geometryState.uvBuf; const phongMaterial = (materialState.type === "PhongMaterial"); const metallicMaterial = (materialState.type === "MetallicMaterial"); const specularMaterial = (materialState.type === "SpecularMaterial"); const receivesShadow = getReceivesShadow(mesh); scene.gammaInput; // If set, then it expects that all textures and colors are premultiplied gamma. Default is false. const gammaOutput = scene.gammaOutput; // If set, then it expects that all textures and colors need to be outputted in premultiplied gamma. Default is false. const src = []; src.push("#version 300 es"); src.push("// Drawing fragment shader"); src.push("#ifdef GL_FRAGMENT_PRECISION_HIGH"); src.push("precision highp float;"); src.push("precision highp int;"); src.push("#else"); src.push("precision mediump float;"); src.push("precision mediump int;"); src.push("#endif"); if (scene.logarithmicDepthBufferEnabled) { src.push("in float isPerspective;"); src.push("uniform float logDepthBufFC;"); src.push("in float vFragDepth;"); } if (receivesShadow) { src.push("float unpackDepth (vec4 color) {"); src.push(" const vec4 bitShift = vec4(1.0, 1.0/256.0, 1.0/(256.0 * 256.0), 1.0/(256.0*256.0*256.0));"); src.push(" return dot(color, bitShift);"); src.push("}"); } //-------------------------------------------------------------------------------- // GAMMA CORRECTION //-------------------------------------------------------------------------------- src.push("uniform float gammaFactor;"); src.push("vec4 linearToLinear( in vec4 value ) {"); src.push(" return value;"); src.push("}"); src.push("vec4 sRGBToLinear( in vec4 value ) {"); src.push(" return vec4( mix( pow( value.rgb * 0.9478672986 + vec3( 0.0521327014 ), vec3( 2.4 ) ), value.rgb * 0.0773993808, vec3( lessThanEqual( value.rgb, vec3( 0.04045 ) ) ) ), value.w );"); src.push("}"); src.push("vec4 gammaToLinear( in vec4 value) {"); src.push(" return vec4( pow( value.xyz, vec3( gammaFactor ) ), value.w );"); src.push("}"); if (gammaOutput) { src.push("vec4 linearToGamma( in vec4 value, in float gammaFactor ) {"); src.push(" return vec4( pow( value.xyz, vec3( 1.0 / gammaFactor ) ), value.w );"); src.push("}"); } //-------------------------------------------------------------------------------- // USER CLIP PLANES //-------------------------------------------------------------------------------- if (clipping) { src.push("in vec4 vWorldPosition;"); src.push("uniform bool clippable;"); for (var i = 0; i < sectionPlanesState.getNumAllocatedSectionPlanes(); i++) { src.push("uniform bool sectionPlaneActive" + i + ";"); src.push("uniform vec3 sectionPlanePos" + i + ";"); src.push("uniform vec3 sectionPlaneDir" + i + ";"); } } if (normals) { //-------------------------------------------------------------------------------- // LIGHT AND REFLECTION MAP INPUTS // Define here so available globally to shader functions //-------------------------------------------------------------------------------- if (lightsState.lightMaps.length > 0) { src.push("uniform samplerCube lightMap;"); src.push("uniform mat4 viewNormalMatrix;"); } if (lightsState.reflectionMaps.length > 0) { src.push("uniform samplerCube reflectionMap;"); } if (lightsState.lightMaps.length > 0 || lightsState.reflectionMaps.length > 0) { src.push("uniform mat4 viewMatrix;"); } //-------------------------------------------------------------------------------- // SHADING FUNCTIONS //-------------------------------------------------------------------------------- // CONSTANT DEFINITIONS src.push("#define PI 3.14159265359"); src.push("#define RECIPROCAL_PI 0.31830988618"); src.push("#define RECIPROCAL_PI2 0.15915494"); src.push("#define EPSILON 1e-6"); src.push("#define saturate(a) clamp( a, 0.0, 1.0 )"); // UTILITY DEFINITIONS src.push("vec3 inverseTransformDirection(in vec3 dir, in mat4 matrix) {"); src.push(" return normalize( ( vec4( dir, 0.0 ) * matrix ).xyz );"); src.push("}"); // STRUCTURES src.push("struct IncidentLight {"); src.push(" vec3 color;"); src.push(" vec3 direction;"); src.push("};"); src.push("struct ReflectedLight {"); src.push(" vec3 diffuse;"); src.push(" vec3 specular;"); src.push("};"); src.push("struct Geometry {"); src.push(" vec3 position;"); src.push(" vec3 viewNormal;"); src.push(" vec3 worldNormal;"); src.push(" vec3 viewEyeDir;"); src.push("};"); src.push("struct Material {"); src.push(" vec3 diffuseColor;"); src.push(" float specularRoughness;"); src.push(" vec3 specularColor;"); src.push(" float shine;"); // Only used for Phong src.push("};"); // COMMON UTILS if (phongMaterial) { if (lightsState.lightMaps.length > 0 || lightsState.reflectionMaps.length > 0) { src.push("void computePhongLightMapping(const in Geometry geometry, const in Material material, inout ReflectedLight reflectedLight) {"); if (lightsState.lightMaps.length > 0) { src.push(" vec3 irradiance = " + TEXTURE_DECODE_FUNCS$1[lightsState.lightMaps[0].encoding] + "(texture(lightMap, geometry.worldNormal)).rgb;"); src.push(" irradiance *= PI;"); src.push(" vec3 diffuseBRDFContrib = (RECIPROCAL_PI * material.diffuseColor);"); src.push(" reflectedLight.diffuse += irradiance * diffuseBRDFContrib;"); } if (lightsState.reflectionMaps.length > 0) { src.push(" vec3 reflectVec = reflect(-geometry.viewEyeDir, geometry.viewNormal);"); src.push(" vec3 radiance = texture(reflectionMap, reflectVec).rgb * 0.2;"); src.push(" radiance *= PI;"); src.push(" reflectedLight.specular += radiance;"); } src.push("}"); } src.push("void computePhongLighting(const in IncidentLight directLight, const in Geometry geometry, const in Material material, inout ReflectedLight reflectedLight) {"); src.push(" float dotNL = saturate(dot(geometry.viewNormal, directLight.direction));"); src.push(" vec3 irradiance = dotNL * directLight.color * PI;"); src.push(" reflectedLight.diffuse += irradiance * (RECIPROCAL_PI * material.diffuseColor);"); src.push(" reflectedLight.specular += directLight.color * material.specularColor * pow(max(dot(reflect(-directLight.direction, -geometry.viewNormal), geometry.viewEyeDir), 0.0), material.shine);"); src.push("}"); } if (metallicMaterial || specularMaterial) { // IRRADIANCE EVALUATION src.push("float GGXRoughnessToBlinnExponent(const in float ggxRoughness) {"); src.push(" float r = ggxRoughness + 0.0001;"); src.push(" return (2.0 / (r * r) - 2.0);"); src.push("}"); src.push("float getSpecularMIPLevel(const in float blinnShininessExponent, const in int maxMIPLevel) {"); src.push(" float maxMIPLevelScalar = float( maxMIPLevel );"); src.push(" float desiredMIPLevel = maxMIPLevelScalar - 0.79248 - 0.5 * log2( ( blinnShininessExponent * blinnShininessExponent ) + 1.0 );"); src.push(" return clamp( desiredMIPLevel, 0.0, maxMIPLevelScalar );"); src.push("}"); if (lightsState.reflectionMaps.length > 0) { src.push("vec3 getLightProbeIndirectRadiance(const in vec3 reflectVec, const in float blinnShininessExponent, const in int maxMIPLevel) {"); src.push(" float mipLevel = 0.5 * getSpecularMIPLevel(blinnShininessExponent, maxMIPLevel);"); //TODO: a random factor - fix this src.push(" vec3 envMapColor = " + TEXTURE_DECODE_FUNCS$1[lightsState.reflectionMaps[0].encoding] + "(texture(reflectionMap, reflectVec, mipLevel)).rgb;"); src.push(" return envMapColor;"); src.push("}"); } // SPECULAR BRDF EVALUATION src.push("vec3 F_Schlick(const in vec3 specularColor, const in float dotLH) {"); src.push(" float fresnel = exp2( ( -5.55473 * dotLH - 6.98316 ) * dotLH );"); src.push(" return ( 1.0 - specularColor ) * fresnel + specularColor;"); src.push("}"); src.push("float G_GGX_Smith(const in float alpha, const in float dotNL, const in float dotNV) {"); src.push(" float a2 = ( alpha * alpha );"); src.push(" float gl = dotNL + sqrt( a2 + ( 1.0 - a2 ) * ( dotNL * dotNL ) );"); src.push(" float gv = dotNV + sqrt( a2 + ( 1.0 - a2 ) * ( dotNV * dotNV ) );"); src.push(" return 1.0 / ( gl * gv );"); src.push("}"); src.push("float G_GGX_SmithCorrelated(const in float alpha, const in float dotNL, const in float dotNV) {"); src.push(" float a2 = ( alpha * alpha );"); src.push(" float gv = dotNL * sqrt( a2 + ( 1.0 - a2 ) * ( dotNV * dotNV ) );"); src.push(" float gl = dotNV * sqrt( a2 + ( 1.0 - a2 ) * ( dotNL * dotNL ) );"); src.push(" return 0.5 / max( gv + gl, EPSILON );"); src.push("}"); src.push("float D_GGX(const in float alpha, const in float dotNH) {"); src.push(" float a2 = ( alpha * alpha );"); src.push(" float denom = ( dotNH * dotNH) * ( a2 - 1.0 ) + 1.0;"); src.push(" return RECIPROCAL_PI * a2 / ( denom * denom);"); src.push("}"); src.push("vec3 BRDF_Specular_GGX(const in IncidentLight incidentLight, const in Geometry geometry, const in vec3 specularColor, const in float roughness) {"); src.push(" float alpha = ( roughness * roughness );"); src.push(" vec3 halfDir = normalize( incidentLight.direction + geometry.viewEyeDir );"); src.push(" float dotNL = saturate( dot( geometry.viewNormal, incidentLight.direction ) );"); src.push(" float dotNV = saturate( dot( geometry.viewNormal, geometry.viewEyeDir ) );"); src.push(" float dotNH = saturate( dot( geometry.viewNormal, halfDir ) );"); src.push(" float dotLH = saturate( dot( incidentLight.direction, halfDir ) );"); src.push(" vec3 F = F_Schlick( specularColor, dotLH );"); src.push(" float G = G_GGX_SmithCorrelated( alpha, dotNL, dotNV );"); src.push(" float D = D_GGX( alpha, dotNH );"); src.push(" return F * (G * D);"); src.push("}"); src.push("vec3 BRDF_Specular_GGX_Environment(const in Geometry geometry, const in vec3 specularColor, const in float roughness) {"); src.push(" float dotNV = saturate(dot(geometry.viewNormal, geometry.viewEyeDir));"); src.push(" const vec4 c0 = vec4( -1, -0.0275, -0.572, 0.022);"); src.push(" const vec4 c1 = vec4( 1, 0.0425, 1.04, -0.04);"); src.push(" vec4 r = roughness * c0 + c1;"); src.push(" float a004 = min(r.x * r.x, exp2(-9.28 * dotNV)) * r.x + r.y;"); src.push(" vec2 AB = vec2(-1.04, 1.04) * a004 + r.zw;"); src.push(" return specularColor * AB.x + AB.y;"); src.push("}"); if (lightsState.lightMaps.length > 0 || lightsState.reflectionMaps.length > 0) { src.push("void computePBRLightMapping(const in Geometry geometry, const in Material material, inout ReflectedLight reflectedLight) {"); if (lightsState.lightMaps.length > 0) { src.push(" vec3 irradiance = sRGBToLinear(texture(lightMap, geometry.worldNormal)).rgb;"); src.push(" irradiance *= PI;"); src.push(" vec3 diffuseBRDFContrib = (RECIPROCAL_PI * material.diffuseColor);"); src.push(" reflectedLight.diffuse += irradiance * diffuseBRDFContrib;"); // src.push(" reflectedLight.diffuse = vec3(1.0, 0.0, 0.0);"); } if (lightsState.reflectionMaps.length > 0) { src.push(" vec3 reflectVec = reflect(-geometry.viewEyeDir, geometry.viewNormal);"); src.push(" reflectVec = inverseTransformDirection(reflectVec, viewMatrix);"); src.push(" float blinnExpFromRoughness = GGXRoughnessToBlinnExponent(material.specularRoughness);"); src.push(" vec3 radiance = getLightProbeIndirectRadiance(reflectVec, blinnExpFromRoughness, 8);"); src.push(" vec3 specularBRDFContrib = BRDF_Specular_GGX_Environment(geometry, material.specularColor, material.specularRoughness);"); src.push(" reflectedLight.specular += radiance * specularBRDFContrib;"); } src.push("}"); } // MAIN LIGHTING COMPUTATION FUNCTION src.push("void computePBRLighting(const in IncidentLight incidentLight, const in Geometry geometry, const in Material material, inout ReflectedLight reflectedLight) {"); src.push(" float dotNL = saturate(dot(geometry.viewNormal, incidentLight.direction));"); src.push(" vec3 irradiance = dotNL * incidentLight.color * PI;"); src.push(" reflectedLight.diffuse += irradiance * (RECIPROCAL_PI * material.diffuseColor);"); src.push(" reflectedLight.specular += irradiance * BRDF_Specular_GGX(incidentLight, geometry, material.specularColor, material.specularRoughness);"); src.push("}"); } // (metallicMaterial || specularMaterial) } // geometry.normals //-------------------------------------------------------------------------------- // GEOMETRY INPUTS //-------------------------------------------------------------------------------- src.push("in vec3 vViewPosition;"); if (geometryState.colors) { src.push("in vec4 vColor;"); } if (uvs && ((normals && material._normalMap) || material._ambientMap || material._baseColorMap || material._diffuseMap || material._emissiveMap || material._metallicMap || material._roughnessMap || material._metallicRoughnessMap || material._specularMap || material._glossinessMap || material._specularGlossinessMap || material._occlusionMap || material._alphaMap)) { src.push("in vec2 vUV;"); } if (normals) { if (lightsState.lightMaps.length > 0) { src.push("in vec3 vWorldNormal;"); } src.push("in vec3 vViewNormal;"); } //-------------------------------------------------------------------------------- // MATERIAL CHANNEL INPUTS //-------------------------------------------------------------------------------- if (materialState.ambient) { src.push("uniform vec3 materialAmbient;"); } if (materialState.baseColor) { src.push("uniform vec3 materialBaseColor;"); } if (materialState.alpha !== undefined && materialState.alpha !== null) { src.push("uniform vec4 materialAlphaModeCutoff;"); // [alpha, alphaMode, alphaCutoff] } if (materialState.emissive) { src.push("uniform vec3 materialEmissive;"); } if (materialState.diffuse) { src.push("uniform vec3 materialDiffuse;"); } if (materialState.glossiness !== undefined && materialState.glossiness !== null) { src.push("uniform float materialGlossiness;"); } if (materialState.shininess !== undefined && materialState.shininess !== null) { src.push("uniform float materialShininess;"); // Phong channel } if (materialState.specular) { src.push("uniform vec3 materialSpecular;"); } if (materialState.metallic !== undefined && materialState.metallic !== null) { src.push("uniform float materialMetallic;"); } if (materialState.roughness !== undefined && materialState.roughness !== null) { src.push("uniform float materialRoughness;"); } if (materialState.specularF0 !== undefined && materialState.specularF0 !== null) { src.push("uniform float materialSpecularF0;"); } //-------------------------------------------------------------------------------- // MATERIAL TEXTURE INPUTS //-------------------------------------------------------------------------------- if (uvs && material._ambientMap) { src.push("uniform sampler2D ambientMap;"); if (material._ambientMap._state.matrix) { src.push("uniform mat4 ambientMapMatrix;"); } } if (uvs && material._baseColorMap) { src.push("uniform sampler2D baseColorMap;"); if (material._baseColorMap._state.matrix) { src.push("uniform mat4 baseColorMapMatrix;"); } } if (uvs && material._diffuseMap) { src.push("uniform sampler2D diffuseMap;"); if (material._diffuseMap._state.matrix) { src.push("uniform mat4 diffuseMapMatrix;"); } } if (uvs && material._emissiveMap) { src.push("uniform sampler2D emissiveMap;"); if (material._emissiveMap._state.matrix) { src.push("uniform mat4 emissiveMapMatrix;"); } } if (normals && uvs && material._metallicMap) { src.push("uniform sampler2D metallicMap;"); if (material._metallicMap._state.matrix) { src.push("uniform mat4 metallicMapMatrix;"); } } if (normals && uvs && material._roughnessMap) { src.push("uniform sampler2D roughnessMap;"); if (material._roughnessMap._state.matrix) { src.push("uniform mat4 roughnessMapMatrix;"); } } if (normals && uvs && material._metallicRoughnessMap) { src.push("uniform sampler2D metallicRoughnessMap;"); if (material._metallicRoughnessMap._state.matrix) { src.push("uniform mat4 metallicRoughnessMapMatrix;"); } } if (normals && material._normalMap) { src.push("uniform sampler2D normalMap;"); if (material._normalMap._state.matrix) { src.push("uniform mat4 normalMapMatrix;"); } src.push("vec3 perturbNormal2Arb( vec3 eye_pos, vec3 surf_norm, vec2 uv ) {"); src.push(" vec3 q0 = vec3( dFdx( eye_pos.x ), dFdx( eye_pos.y ), dFdx( eye_pos.z ) );"); src.push(" vec3 q1 = vec3( dFdy( eye_pos.x ), dFdy( eye_pos.y ), dFdy( eye_pos.z ) );"); src.push(" vec2 st0 = dFdx( uv.st );"); src.push(" vec2 st1 = dFdy( uv.st );"); src.push(" vec3 S = normalize( q0 * st1.t - q1 * st0.t );"); src.push(" vec3 T = normalize( -q0 * st1.s + q1 * st0.s );"); src.push(" vec3 N = normalize( surf_norm );"); src.push(" vec3 mapN = texture( normalMap, uv ).xyz * 2.0 - 1.0;"); src.push(" mat3 tsn = mat3( S, T, N );"); // src.push(" mapN *= 3.0;"); src.push(" return normalize( tsn * mapN );"); src.push("}"); } if (uvs && material._occlusionMap) { src.push("uniform sampler2D occlusionMap;"); if (material._occlusionMap._state.matrix) { src.push("uniform mat4 occlusionMapMatrix;"); } } if (uvs && material._alphaMap) { src.push("uniform sampler2D alphaMap;"); if (material._alphaMap._state.matrix) { src.push("uniform mat4 alphaMapMatrix;"); } } if (normals && uvs && material._specularMap) { src.push("uniform sampler2D specularMap;"); if (material._specularMap._state.matrix) { src.push("uniform mat4 specularMapMatrix;"); } } if (normals && uvs && material._glossinessMap) { src.push("uniform sampler2D glossinessMap;"); if (material._glossinessMap._state.matrix) { src.push("uniform mat4 glossinessMapMatrix;"); } } if (normals && uvs && material._specularGlossinessMap) { src.push("uniform sampler2D materialSpecularGlossinessMap;"); if (material._specularGlossinessMap._state.matrix) { src.push("uniform mat4 materialSpecularGlossinessMapMatrix;"); } } //-------------------------------------------------------------------------------- // MATERIAL FRESNEL INPUTS //-------------------------------------------------------------------------------- if (normals && (material._diffuseFresnel || material._specularFresnel || material._alphaFresnel || material._emissiveFresnel || material._reflectivityFresnel)) { src.push("float fresnel(vec3 eyeDir, vec3 normal, float edgeBias, float centerBias, float power) {"); src.push(" float fr = abs(dot(eyeDir, normal));"); src.push(" float finalFr = clamp((fr - edgeBias) / (centerBias - edgeBias), 0.0, 1.0);"); src.push(" return pow(finalFr, power);"); src.push("}"); if (material._diffuseFresnel) { src.push("uniform float diffuseFresnelCenterBias;"); src.push("uniform float diffuseFresnelEdgeBias;"); src.push("uniform float diffuseFresnelPower;"); src.push("uniform vec3 diffuseFresnelCenterColor;"); src.push("uniform vec3 diffuseFresnelEdgeColor;"); } if (material._specularFresnel) { src.push("uniform float specularFresnelCenterBias;"); src.push("uniform float specularFresnelEdgeBias;"); src.push("uniform float specularFresnelPower;"); src.push("uniform vec3 specularFresnelCenterColor;"); src.push("uniform vec3 specularFresnelEdgeColor;"); } if (material._alphaFresnel) { src.push("uniform float alphaFresnelCenterBias;"); src.push("uniform float alphaFresnelEdgeBias;"); src.push("uniform float alphaFresnelPower;"); src.push("uniform vec3 alphaFresnelCenterColor;"); src.push("uniform vec3 alphaFresnelEdgeColor;"); } if (material._reflectivityFresnel) { src.push("uniform float materialSpecularF0FresnelCenterBias;"); src.push("uniform float materialSpecularF0FresnelEdgeBias;"); src.push("uniform float materialSpecularF0FresnelPower;"); src.push("uniform vec3 materialSpecularF0FresnelCenterColor;"); src.push("uniform vec3 materialSpecularF0FresnelEdgeColor;"); } if (material._emissiveFresnel) { src.push("uniform float emissiveFresnelCenterBias;"); src.push("uniform float emissiveFresnelEdgeBias;"); src.push("uniform float emissiveFresnelPower;"); src.push("uniform vec3 emissiveFresnelCenterColor;"); src.push("uniform vec3 emissiveFresnelEdgeColor;"); } } //-------------------------------------------------------------------------------- // LIGHT SOURCES //-------------------------------------------------------------------------------- src.push("uniform vec4 lightAmbient;"); if (normals) { for (let i = 0, len = lightsState.lights.length; i < len; i++) { // Light sources const light = lightsState.lights[i]; if (light.type === "ambient") { continue; } src.push("uniform vec4 lightColor" + i + ";"); if (light.type === "point") { src.push("uniform vec3 lightAttenuation" + i + ";"); } if (light.type === "dir" && light.space === "view") { src.push("uniform vec3 lightDir" + i + ";"); } if (light.type === "point" && light.space === "view") { src.push("uniform vec3 lightPos" + i + ";"); } else { src.push("in vec4 vViewLightReverseDirAndDist" + i + ";"); } } } if (receivesShadow) { // Variance castsShadow mapping filter // src.push("float linstep(float low, float high, float v){"); // src.push(" return clamp((v-low)/(high-low), 0.0, 1.0);"); // src.push("}"); // // src.push("float VSM(sampler2D depths, vec2 uv, float compare){"); // src.push(" vec2 moments = texture(depths, uv).xy;"); // src.push(" float p = smoothstep(compare-0.02, compare, moments.x);"); // src.push(" float variance = max(moments.y - moments.x*moments.x, -0.001);"); // src.push(" float d = compare - moments.x;"); // src.push(" float p_max = linstep(0.2, 1.0, variance / (variance + d*d));"); // src.push(" return clamp(max(p, p_max), 0.0, 1.0);"); // src.push("}"); for (let i = 0, len = lightsState.lights.length; i < len; i++) { // Light sources if (lightsState.lights[i].castsShadow) { src.push("in vec4 vShadowPosFromLight" + i + ";"); src.push("uniform sampler2D shadowMap" + i + ";"); } } } src.push("uniform vec4 colorize;"); //================================================================================ // MAIN //================================================================================ src.push("out vec4 outColor;"); src.push("void main(void) {"); if (clipping) { src.push("if (clippable) {"); src.push(" float dist = 0.0;"); for (var i = 0; i < sectionPlanesState.getNumAllocatedSectionPlanes(); i++) { src.push("if (sectionPlaneActive" + i + ") {"); src.push(" dist += clamp(dot(-sectionPlaneDir" + i + ".xyz, vWorldPosition.xyz - sectionPlanePos" + i + ".xyz), 0.0, 1000.0);"); src.push("}"); } src.push(" if (dist > 0.0) { discard; }"); src.push("}"); } if (geometryState.primitiveName === "points") { src.push("vec2 cxy = 2.0 * gl_PointCoord - 1.0;"); src.push("float r = dot(cxy, cxy);"); src.push("if (r > 1.0) {"); src.push(" discard;"); src.push("}"); } src.push("float occlusion = 1.0;"); if (materialState.ambient) { src.push("vec3 ambientColor = materialAmbient;"); } else { src.push("vec3 ambientColor = vec3(1.0, 1.0, 1.0);"); } if (materialState.diffuse) { src.push("vec3 diffuseColor = materialDiffuse;"); } else if (materialState.baseColor) { src.push("vec3 diffuseColor = materialBaseColor;"); } else { src.push("vec3 diffuseColor = vec3(1.0, 1.0, 1.0);"); } if (geometryState.colors) { src.push("diffuseColor *= vColor.rgb;"); } if (materialState.emissive) { src.push("vec3 emissiveColor = materialEmissive;"); // Emissive default is (0,0,0), so initializing here } else { src.push("vec3 emissiveColor = vec3(0.0, 0.0, 0.0);"); } if (materialState.specular) { src.push("vec3 specular = materialSpecular;"); } else { src.push("vec3 specular = vec3(1.0, 1.0, 1.0);"); } if (materialState.alpha !== undefined) { src.push("float alpha = materialAlphaModeCutoff[0];"); } else { src.push("float alpha = 1.0;"); } if (geometryState.colors) { src.push("alpha *= vColor.a;"); } if (materialState.glossiness !== undefined) { src.push("float glossiness = materialGlossiness;"); } else { src.push("float glossiness = 1.0;"); } if (materialState.metallic !== undefined) { src.push("float metallic = materialMetallic;"); } else { src.push("float metallic = 1.0;"); } if (materialState.roughness !== undefined) { src.push("float roughness = materialRoughness;"); } else { src.push("float roughness = 1.0;"); } if (materialState.specularF0 !== undefined) { src.push("float specularF0 = materialSpecularF0;"); } else { src.push("float specularF0 = 1.0;"); } //-------------------------------------------------------------------------------- // TEXTURING //-------------------------------------------------------------------------------- if (uvs && ((normals && material._normalMap) || material._ambientMap || material._baseColorMap || material._diffuseMap || material._occlusionMap || material._emissiveMap || material._metallicMap || material._roughnessMap || material._metallicRoughnessMap || material._specularMap || material._glossinessMap || material._specularGlossinessMap || material._alphaMap)) { src.push("vec4 texturePos = vec4(vUV.s, vUV.t, 1.0, 1.0);"); src.push("vec2 textureCoord;"); } if (uvs && material._ambientMap) { if (material._ambientMap._state.matrix) { src.push("textureCoord = (ambientMapMatrix * texturePos).xy;"); } else { src.push("textureCoord = texturePos.xy;"); } src.push("vec4 ambientTexel = texture(ambientMap, textureCoord).rgb;"); src.push("ambientTexel = " + TEXTURE_DECODE_FUNCS$1[material._ambientMap._state.encoding] + "(ambientTexel);"); src.push("ambientColor *= ambientTexel.rgb;"); } if (uvs && material._diffuseMap) { if (material._diffuseMap._state.matrix) { src.push("textureCoord = (diffuseMapMatrix * texturePos).xy;"); } else { src.push("textureCoord = texturePos.xy;"); } src.push("vec4 diffuseTexel = texture(diffuseMap, textureCoord);"); src.push("diffuseTexel = " + TEXTURE_DECODE_FUNCS$1[material._diffuseMap._state.encoding] + "(diffuseTexel);"); src.push("diffuseColor *= diffuseTexel.rgb;"); src.push("alpha *= diffuseTexel.a;"); } if (uvs && material._baseColorMap) { if (material._baseColorMap._state.matrix) { src.push("textureCoord = (baseColorMapMatrix * texturePos).xy;"); } else { src.push("textureCoord = texturePos.xy;"); } src.push("vec4 baseColorTexel = texture(baseColorMap, textureCoord);"); src.push("baseColorTexel = " + TEXTURE_DECODE_FUNCS$1[material._baseColorMap._state.encoding] + "(baseColorTexel);"); src.push("diffuseColor *= baseColorTexel.rgb;"); src.push("alpha *= baseColorTexel.a;"); } if (uvs && material._emissiveMap) { if (material._emissiveMap._state.matrix) { src.push("textureCoord = (emissiveMapMatrix * texturePos).xy;"); } else { src.push("textureCoord = texturePos.xy;"); } src.push("vec4 emissiveTexel = texture(emissiveMap, textureCoord);"); src.push("emissiveTexel = " + TEXTURE_DECODE_FUNCS$1[material._emissiveMap._state.encoding] + "(emissiveTexel);"); src.push("emissiveColor = emissiveTexel.rgb;"); } if (uvs && material._alphaMap) { if (material._alphaMap._state.matrix) { src.push("textureCoord = (alphaMapMatrix * texturePos).xy;"); } else { src.push("textureCoord = texturePos.xy;"); } src.push("alpha *= texture(alphaMap, textureCoord).r;"); } if (uvs && material._occlusionMap) { if (material._occlusionMap._state.matrix) { src.push("textureCoord = (occlusionMapMatrix * texturePos).xy;"); } else { src.push("textureCoord = texturePos.xy;"); } src.push("occlusion *= texture(occlusionMap, textureCoord).r;"); } if (normals && ((lightsState.lights.length > 0) || lightsState.lightMaps.length > 0 || lightsState.reflectionMaps.length > 0)) { //-------------------------------------------------------------------------------- // SHADING //-------------------------------------------------------------------------------- if (uvs && material._normalMap) { if (material._normalMap._state.matrix) { src.push("textureCoord = (normalMapMatrix * texturePos).xy;"); } else { src.push("textureCoord = texturePos.xy;"); } src.push("vec3 viewNormal = perturbNormal2Arb( vViewPosition, normalize(vViewNormal), textureCoord );"); } else { src.push("vec3 viewNormal = normalize(vViewNormal);"); } if (uvs && material._specularMap) { if (material._specularMap._state.matrix) { src.push("textureCoord = (specularMapMatrix * texturePos).xy;"); } else { src.push("textureCoord = texturePos.xy;"); } src.push("specular *= texture(specularMap, textureCoord).rgb;"); } if (uvs && material._glossinessMap) { if (material._glossinessMap._state.matrix) { src.push("textureCoord = (glossinessMapMatrix * texturePos).xy;"); } else { src.push("textureCoord = texturePos.xy;"); } src.push("glossiness *= texture(glossinessMap, textureCoord).r;"); } if (uvs && material._specularGlossinessMap) { if (material._specularGlossinessMap._state.matrix) { src.push("textureCoord = (materialSpecularGlossinessMapMatrix * texturePos).xy;"); } else { src.push("textureCoord = texturePos.xy;"); } src.push("vec4 specGlossRGB = texture(materialSpecularGlossinessMap, textureCoord).rgba;"); // TODO: what if only RGB texture? src.push("specular *= specGlossRGB.rgb;"); src.push("glossiness *= specGlossRGB.a;"); } if (uvs && material._metallicMap) { if (material._metallicMap._state.matrix) { src.push("textureCoord = (metallicMapMatrix * texturePos).xy;"); } else { src.push("textureCoord = texturePos.xy;"); } src.push("metallic *= texture(metallicMap, textureCoord).r;"); } if (uvs && material._roughnessMap) { if (material._roughnessMap._state.matrix) { src.push("textureCoord = (roughnessMapMatrix * texturePos).xy;"); } else { src.push("textureCoord = texturePos.xy;"); } src.push("roughness *= texture(roughnessMap, textureCoord).r;"); } if (uvs && material._metallicRoughnessMap) { if (material._metallicRoughnessMap._state.matrix) { src.push("textureCoord = (metallicRoughnessMapMatrix * texturePos).xy;"); } else { src.push("textureCoord = texturePos.xy;"); } src.push("vec3 metalRoughRGB = texture(metallicRoughnessMap, textureCoord).rgb;"); src.push("metallic *= metalRoughRGB.b;"); src.push("roughness *= metalRoughRGB.g;"); } src.push("vec3 viewEyeDir = normalize(-vViewPosition);"); if (material._diffuseFresnel) { src.push("float diffuseFresnel = fresnel(viewEyeDir, viewNormal, diffuseFresnelEdgeBias, diffuseFresnelCenterBias, diffuseFresnelPower);"); src.push("diffuseColor *= mix(diffuseFresnelEdgeColor, diffuseFresnelCenterColor, diffuseFresnel);"); } if (material._specularFresnel) { src.push("float specularFresnel = fresnel(viewEyeDir, viewNormal, specularFresnelEdgeBias, specularFresnelCenterBias, specularFresnelPower);"); src.push("specular *= mix(specularFresnelEdgeColor, specularFresnelCenterColor, specularFresnel);"); } if (material._alphaFresnel) { src.push("float alphaFresnel = fresnel(viewEyeDir, viewNormal, alphaFresnelEdgeBias, alphaFresnelCenterBias, alphaFresnelPower);"); src.push("alpha *= mix(alphaFresnelEdgeColor.r, alphaFresnelCenterColor.r, alphaFresnel);"); } if (material._emissiveFresnel) { src.push("float emissiveFresnel = fresnel(viewEyeDir, viewNormal, emissiveFresnelEdgeBias, emissiveFresnelCenterBias, emissiveFresnelPower);"); src.push("emissiveColor *= mix(emissiveFresnelEdgeColor, emissiveFresnelCenterColor, emissiveFresnel);"); } src.push("if (materialAlphaModeCutoff[1] == 1.0 && alpha < materialAlphaModeCutoff[2]) {"); // ie. (alphaMode == "mask" && alpha < alphaCutoff) src.push(" discard;"); // TODO: Discard earlier within this shader? src.push("}"); // PREPARE INPUTS FOR SHADER FUNCTIONS src.push("IncidentLight light;"); src.push("Material material;"); src.push("Geometry geometry;"); src.push("ReflectedLight reflectedLight = ReflectedLight(vec3(0.0,0.0,0.0), vec3(0.0,0.0,0.0));"); src.push("vec3 viewLightDir;"); if (phongMaterial) { src.push("material.diffuseColor = diffuseColor;"); src.push("material.specularColor = specular;"); src.push("material.shine = materialShininess;"); } if (specularMaterial) { src.push("float oneMinusSpecularStrength = 1.0 - max(max(specular.r, specular.g ),specular.b);"); // Energy conservation src.push("material.diffuseColor = diffuseColor * oneMinusSpecularStrength;"); src.push("material.specularRoughness = clamp( 1.0 - glossiness, 0.04, 1.0 );"); src.push("material.specularColor = specular;"); } if (metallicMaterial) { src.push("float dielectricSpecular = 0.16 * specularF0 * specularF0;"); src.push("material.diffuseColor = diffuseColor * (1.0 - dielectricSpecular) * (1.0 - metallic);"); src.push("material.specularRoughness = clamp(roughness, 0.04, 1.0);"); src.push("material.specularColor = mix(vec3(dielectricSpecular), diffuseColor, metallic);"); } src.push("geometry.position = vViewPosition;"); if (lightsState.lightMaps.length > 0) { src.push("geometry.worldNormal = normalize(vWorldNormal);"); } src.push("geometry.viewNormal = viewNormal;"); src.push("geometry.viewEyeDir = viewEyeDir;"); // ENVIRONMENT AND REFLECTION MAP SHADING if ((phongMaterial) && (lightsState.lightMaps.length > 0 || lightsState.reflectionMaps.length > 0)) { src.push("computePhongLightMapping(geometry, material, reflectedLight);"); } if ((specularMaterial || metallicMaterial) && (lightsState.lightMaps.length > 0 || lightsState.reflectionMaps.length > 0)) { src.push("computePBRLightMapping(geometry, material, reflectedLight);"); } // LIGHT SOURCE SHADING src.push("float shadow = 1.0;"); // if (receivesShadow) { // // src.push("float lightDepth2 = clamp(length(lightPos)/40.0, 0.0, 1.0);"); // src.push("float illuminated = VSM(sLightDepth, lightUV, lightDepth2);"); // src.push("float shadowAcneRemover = 0.007;"); src.push("vec3 fragmentDepth;"); src.push("float texelSize = 1.0 / 1024.0;"); src.push("float amountInLight = 0.0;"); src.push("vec3 shadowCoord;"); src.push('vec4 rgbaDepth;'); src.push("float depth;"); for (let i = 0, len = lightsState.lights.length; i < len; i++) { const light = lightsState.lights[i]; if (light.type === "ambient") { continue; } if (light.type === "dir" && light.space === "view") { src.push("viewLightDir = -normalize(lightDir" + i + ");"); } else if (light.type === "point" && light.space === "view") { src.push("viewLightDir = normalize(lightPos" + i + " - vViewPosition);"); //src.push("tmpVec3 = lightPos" + i + ".xyz - viewPosition.xyz;"); //src.push("lightDist = abs(length(tmpVec3));"); } else { src.push("viewLightDir = normalize(vViewLightReverseDirAndDist" + i + ".xyz);"); // If normal mapping, the fragment->light vector will be in tangent space } if (receivesShadow && light.castsShadow) { // if (true) { // src.push('shadowCoord = (vShadowPosFromLight' + i + '.xyz/vShadowPosFromLight' + i + '.w)/2.0 + 0.5;'); // src.push("lightDepth2 = clamp(length(vec3[0.0, 20.0, 20.0])/40.0, 0.0, 1.0);"); // src.push("castsShadow *= VSM(shadowMap' + i + ', shadowCoord, lightDepth2);"); // } // // if (false) { // // PCF src.push("shadow = 0.0;"); src.push("fragmentDepth = vShadowPosFromLight" + i + ".xyz;"); src.push("fragmentDepth.z -= shadowAcneRemover;"); src.push("for (int x = -3; x <= 3; x++) {"); src.push(" for (int y = -3; y <= 3; y++) {"); src.push(" float texelDepth = unpackDepth(texture(shadowMap" + i + ", fragmentDepth.xy + vec2(x, y) * texelSize));"); src.push(" if (fragmentDepth.z < texelDepth) {"); src.push(" shadow += 1.0;"); src.push(" }"); src.push(" }"); src.push("}"); src.push("shadow = shadow / 9.0;"); src.push("light.color = lightColor" + i + ".rgb * (lightColor" + i + ".a * shadow);"); // a is intensity // // } // // if (false){ // // src.push("shadow = 1.0;"); // // src.push('shadowCoord = (vShadowPosFromLight' + i + '.xyz/vShadowPosFromLight' + i + '.w)/2.0 + 0.5;'); // // src.push('shadow -= (shadowCoord.z > unpackDepth(texture(shadowMap' + i + ', shadowCoord.xy + vec2( -0.94201624, -0.39906216 ) / 700.0)) + 0.0015) ? 0.2 : 0.0;'); // src.push('shadow -= (shadowCoord.z > unpackDepth(texture(shadowMap' + i + ', shadowCoord.xy + vec2( 0.94558609, -0.76890725 ) / 700.0)) + 0.0015) ? 0.2 : 0.0;'); // src.push('shadow -= (shadowCoord.z > unpackDepth(texture(shadowMap' + i + ', shadowCoord.xy + vec2( -0.094184101, -0.92938870 ) / 700.0)) + 0.0015) ? 0.2 : 0.0;'); // src.push('shadow -= (shadowCoord.z > unpackDepth(texture(shadowMap' + i + ', shadowCoord.xy + vec2( 0.34495938, 0.29387760 ) / 700.0)) + 0.0015) ? 0.2 : 0.0;'); // // src.push("light.color = lightColor" + i + ".rgb * (lightColor" + i + ".a * shadow);"); // } } else { src.push("light.color = lightColor" + i + ".rgb * (lightColor" + i + ".a );"); // a is intensity } src.push("light.direction = viewLightDir;"); if (phongMaterial) { src.push("computePhongLighting(light, geometry, material, reflectedLight);"); } if (specularMaterial || metallicMaterial) { src.push("computePBRLighting(light, geometry, material, reflectedLight);"); } } //src.push("reflectedLight.diffuse *= shadow;"); // COMBINE TERMS if (phongMaterial) { src.push("vec3 outgoingLight = (lightAmbient.rgb * lightAmbient.a * diffuseColor) + ((occlusion * (( reflectedLight.diffuse + reflectedLight.specular)))) + emissiveColor;"); } else { src.push("vec3 outgoingLight = (occlusion * (reflectedLight.diffuse)) + (occlusion * reflectedLight.specular) + emissiveColor;"); } } else { //-------------------------------------------------------------------------------- // NO SHADING - EMISSIVE and AMBIENT ONLY //-------------------------------------------------------------------------------- src.push("ambientColor *= (lightAmbient.rgb * lightAmbient.a);"); src.push("vec3 outgoingLight = emissiveColor + ambientColor;"); } src.push("vec4 fragColor = vec4(outgoingLight, alpha) * colorize;"); if (gammaOutput) { src.push("fragColor = linearToGamma(fragColor, gammaFactor);"); } src.push("outColor = fragColor;"); if (scene.logarithmicDepthBufferEnabled) { src.push("gl_FragDepth = isPerspective == 0.0 ? gl_FragCoord.z : log2( vFragDepth ) * logDepthBufFC * 0.5;"); } src.push("}"); return src; } /** * @desc Represents a vertex or fragment stage within a {@link Program}. * @private */ class Shader { constructor(gl, type, source) { this.allocated = false; this.compiled = false; this.handle = gl.createShader(type); if (!this.handle) { this.errors = [ "Failed to allocate" ]; return; } this.allocated = true; gl.shaderSource(this.handle, source); gl.compileShader(this.handle); this.compiled = gl.getShaderParameter(this.handle, gl.COMPILE_STATUS); if (!this.compiled) { if (!gl.isContextLost()) { // Handled explicitly elsewhere, so won't re-handle here const lines = source.split("\n"); const numberedLines = []; for (let i = 0; i < lines.length; i++) { numberedLines.push((i + 1) + ": " + lines[i] + "\n"); } this.errors = []; this.errors.push(""); this.errors.push(gl.getShaderInfoLog(this.handle)); this.errors = this.errors.concat(numberedLines.join("")); } } } destroy() { } } /** * @desc A low-level component that represents a WebGL Sampler. * @private */ class Sampler { constructor(gl, location) { this.bindTexture = function (texture, unit) { if (texture.bind(unit)) { gl.uniform1i(location, unit); return true; } return false; }; } } /** * @desc Represents a WebGL vertex attribute buffer (VBO). * @private * @param gl {WebGLRenderingContext} The WebGL rendering context. */ class Attribute { constructor(gl, location) { this._gl = gl; this.location = location; } bindArrayBuffer(arrayBuf) { if (!arrayBuf) { return; } arrayBuf.bind(); this._gl.enableVertexAttribArray(this.location); this._gl.vertexAttribPointer(this.location, arrayBuf.itemSize, arrayBuf.itemType, arrayBuf.normalized, arrayBuf.stride, arrayBuf.offset); } } const ids$3 = new Map$1({}); function joinSansComments(srcLines) { const src = []; let line; let n; for (let i = 0, len = srcLines.length; i < len; i++) { line = srcLines[i]; n = line.indexOf("/"); if (n > 0) { if (line.charAt(n + 1) === "/") { line = line.substring(0, n); } } src.push(line); } return src.join("\n"); } function logErrors(errors) { console.error(errors.join("\n")); } /** * @desc Represents a WebGL program. * @private */ class Program { constructor(gl, shaderSource) { this.id = ids$3.addItem({}); this.source = shaderSource; this.init(gl); } init(gl) { this.gl = gl; this.allocated = false; this.compiled = false; this.linked = false; this.validated = false; this.errors = null; this.uniforms = {}; this.samplers = {}; this.attributes = {}; this._vertexShader = new Shader(gl, gl.VERTEX_SHADER, joinSansComments(this.source.vertex)); this._fragmentShader = new Shader(gl, gl.FRAGMENT_SHADER, joinSansComments(this.source.fragment)); if (!this._vertexShader.allocated) { this.errors = ["Vertex shader failed to allocate"].concat(this._vertexShader.errors); logErrors(this.errors); return; } if (!this._fragmentShader.allocated) { this.errors = ["Fragment shader failed to allocate"].concat(this._fragmentShader.errors); logErrors(this.errors); return; } this.allocated = true; if (!this._vertexShader.compiled) { this.errors = ["Vertex shader failed to compile"].concat(this._vertexShader.errors); logErrors(this.errors); return; } if (!this._fragmentShader.compiled) { this.errors = ["Fragment shader failed to compile"].concat(this._fragmentShader.errors); logErrors(this.errors); return; } this.compiled = true; let a; let i; let u; let uName; let location; this.handle = gl.createProgram(); if (!this.handle) { this.errors = ["Failed to allocate program"]; return; } gl.attachShader(this.handle, this._vertexShader.handle); gl.attachShader(this.handle, this._fragmentShader.handle); gl.linkProgram(this.handle); this.linked = gl.getProgramParameter(this.handle, gl.LINK_STATUS); // HACK: Disable validation temporarily // Perhaps we should defer validation until render-time, when the program has values set for all inputs? this.validated = true; if (!this.linked || !this.validated) { this.errors = []; this.errors.push(""); this.errors.push(gl.getProgramInfoLog(this.handle)); this.errors.push("\nVertex shader:\n"); this.errors = this.errors.concat(this.source.vertex); this.errors.push("\nFragment shader:\n"); this.errors = this.errors.concat(this.source.fragment); logErrors(this.errors); return; } const numUniforms = gl.getProgramParameter(this.handle, gl.ACTIVE_UNIFORMS); for (i = 0; i < numUniforms; ++i) { u = gl.getActiveUniform(this.handle, i); if (u) { uName = u.name; if (uName[uName.length - 1] === "\u0000") { uName = uName.substr(0, uName.length - 1); } location = gl.getUniformLocation(this.handle, uName); if ((u.type === gl.SAMPLER_2D) || (u.type === gl.SAMPLER_CUBE) || (u.type === 35682)) { this.samplers[uName] = new Sampler(gl, location); } else if (gl instanceof WebGL2RenderingContext && (u.type === gl.UNSIGNED_INT_SAMPLER_2D || u.type === gl.INT_SAMPLER_2D)) { this.samplers[uName] = new Sampler(gl, location); } else { this.uniforms[uName] = location; } } } const numAttribs = gl.getProgramParameter(this.handle, gl.ACTIVE_ATTRIBUTES); for (i = 0; i < numAttribs; i++) { a = gl.getActiveAttrib(this.handle, i); if (a) { location = gl.getAttribLocation(this.handle, a.name); this.attributes[a.name] = new Attribute(gl, location); } } this.allocated = true; } bind() { if (!this.allocated) { return; } this.gl.useProgram(this.handle); } getLocation(name) { if (!this.allocated) { return; } return this.uniforms[name]; } getAttribute(name) { if (!this.allocated) { return; } return this.attributes[name]; } bindTexture(name, texture, unit) { if (!this.allocated) { return false; } const sampler = this.samplers[name]; if (sampler) { return sampler.bindTexture(texture, unit); } else { return false; } } destroy() { if (!this.allocated) { return; } ids$3.removeItem(this.id); this.gl.deleteProgram(this.handle); this.gl.deleteShader(this._vertexShader.handle); this.gl.deleteShader(this._fragmentShader.handle); this.handle = null; this.attributes = null; this.uniforms = null; this.samplers = null; this.allocated = false; } } /** * @private * @type {{WEBGL: boolean, SUPPORTED_EXTENSIONS: {}}} */ const WEBGL_INFO = { WEBGL: false, SUPPORTED_EXTENSIONS: {} }; const canvas = document.createElement("canvas"); if (canvas) { const gl = canvas.getContext("webgl", {antialias: true}) || canvas.getContext("experimental-webgl", {antialias: true}); WEBGL_INFO.WEBGL = !!gl; if (WEBGL_INFO.WEBGL) { WEBGL_INFO.ANTIALIAS = gl.getContextAttributes().antialias; if (gl.getShaderPrecisionFormat) { if (gl.getShaderPrecisionFormat(gl.FRAGMENT_SHADER, gl.HIGH_FLOAT).precision > 0) { WEBGL_INFO.FS_MAX_FLOAT_PRECISION = "highp"; } else if (gl.getShaderPrecisionFormat(gl.FRAGMENT_SHADER, gl.MEDIUM_FLOAT).precision > 0) { WEBGL_INFO.FS_MAX_FLOAT_PRECISION = "mediump"; } else { WEBGL_INFO.FS_MAX_FLOAT_PRECISION = "lowp"; } } else { WEBGL_INFO.FS_MAX_FLOAT_PRECISION = "mediump"; } WEBGL_INFO.DEPTH_BUFFER_BITS = gl.getParameter(gl.DEPTH_BITS); WEBGL_INFO.MAX_TEXTURE_SIZE = gl.getParameter(gl.MAX_TEXTURE_SIZE); WEBGL_INFO.MAX_CUBE_MAP_SIZE = gl.getParameter(gl.MAX_CUBE_MAP_TEXTURE_SIZE); WEBGL_INFO.MAX_RENDERBUFFER_SIZE = gl.getParameter(gl.MAX_RENDERBUFFER_SIZE); WEBGL_INFO.MAX_TEXTURE_UNITS = gl.getParameter(gl.MAX_COMBINED_TEXTURE_IMAGE_UNITS); WEBGL_INFO.MAX_TEXTURE_IMAGE_UNITS = gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS); WEBGL_INFO.MAX_VERTEX_ATTRIBS = gl.getParameter(gl.MAX_VERTEX_ATTRIBS); WEBGL_INFO.MAX_VERTEX_UNIFORM_VECTORS = gl.getParameter(gl.MAX_VERTEX_UNIFORM_VECTORS); WEBGL_INFO.MAX_FRAGMENT_UNIFORM_VECTORS = gl.getParameter(gl.MAX_FRAGMENT_UNIFORM_VECTORS); WEBGL_INFO.MAX_VARYING_VECTORS = gl.getParameter(gl.MAX_VARYING_VECTORS); gl.getSupportedExtensions().forEach(function (ext) { WEBGL_INFO.SUPPORTED_EXTENSIONS[ext] = true; }); } } /** * @author xeolabs / https://github.com/xeolabs */ const tempVec3a$L = math.vec3(); const ids$2 = new Map$1({}); /** * @private */ const DrawRenderer = function (hash, mesh) { this.id = ids$2.addItem({}); this._hash = hash; this._scene = mesh.scene; this._useCount = 0; this._shaderSource = new DrawShaderSource(mesh); this._allocate(mesh); }; const drawRenderers = {}; DrawRenderer.get = function (mesh) { const scene = mesh.scene; const hash = [ scene.canvas.canvas.id, (scene.gammaInput ? "gi;" : ";") + (scene.gammaOutput ? "go" : ""), scene._lightsState.getHash(), scene._sectionPlanesState.getHash(), mesh._geometry._state.hash, mesh._material._state.hash, mesh._state.drawHash ].join(";"); let renderer = drawRenderers[hash]; if (!renderer) { renderer = new DrawRenderer(hash, mesh); if (renderer.errors) { console.log(renderer.errors.join("\n")); return null; } drawRenderers[hash] = renderer; stats.memory.programs++; } renderer._useCount++; return renderer; }; DrawRenderer.prototype.put = function () { if (--this._useCount === 0) { ids$2.removeItem(this.id); if (this._program) { this._program.destroy(); } delete drawRenderers[this._hash]; stats.memory.programs--; } }; DrawRenderer.prototype.webglContextRestored = function () { this._program = null; }; DrawRenderer.prototype.drawMesh = function (frameCtx, mesh) { if (!this._program) { this._allocate(mesh); } const maxTextureUnits = WEBGL_INFO.MAX_TEXTURE_UNITS; const scene = mesh.scene; const material = mesh._material; const gl = scene.canvas.gl; const program = this._program; const meshState = mesh._state; const materialState = mesh._material._state; const geometryState = mesh._geometry._state; const camera = scene.camera; const origin = mesh.origin; const background = meshState.background; if (frameCtx.lastProgramId !== this._program.id) { frameCtx.lastProgramId = this._program.id; if (background) { gl.depthFunc(gl.LEQUAL); } this._bindProgram(frameCtx); } gl.uniformMatrix4fv(this._uViewMatrix, false, origin ? frameCtx.getRTCViewMatrix(meshState.originHash, origin) : camera.viewMatrix); gl.uniformMatrix4fv(this._uViewNormalMatrix, false, camera.viewNormalMatrix); if (meshState.clippable) { const numAllocatedSectionPlanes = scene._sectionPlanesState.getNumAllocatedSectionPlanes(); const numSectionPlanes = scene._sectionPlanesState.sectionPlanes.length; if (numAllocatedSectionPlanes > 0) { const sectionPlanes = scene._sectionPlanesState.sectionPlanes; const renderFlags = mesh.renderFlags; for (let sectionPlaneIndex = 0; sectionPlaneIndex < numAllocatedSectionPlanes; sectionPlaneIndex++) { const sectionPlaneUniforms = this._uSectionPlanes[sectionPlaneIndex]; if (sectionPlaneUniforms) { if (sectionPlaneIndex < numSectionPlanes) { const active = renderFlags.sectionPlanesActivePerLayer[sectionPlaneIndex]; gl.uniform1i(sectionPlaneUniforms.active, active ? 1 : 0); if (active) { const sectionPlane = sectionPlanes[sectionPlaneIndex]; if (origin) { const rtcSectionPlanePos = getPlaneRTCPos(sectionPlane.dist, sectionPlane.dir, origin, tempVec3a$L); gl.uniform3fv(sectionPlaneUniforms.pos, rtcSectionPlanePos); } else { gl.uniform3fv(sectionPlaneUniforms.pos, sectionPlane.pos); } gl.uniform3fv(sectionPlaneUniforms.dir, sectionPlane.dir); } } else { gl.uniform1i(sectionPlaneUniforms.active, 0); } } } } } if (materialState.id !== this._lastMaterialId) { frameCtx.textureUnit = this._baseTextureUnit; const backfaces = materialState.backfaces; if (frameCtx.backfaces !== backfaces) { if (backfaces) { gl.disable(gl.CULL_FACE); } else { gl.enable(gl.CULL_FACE); } frameCtx.backfaces = backfaces; } const frontface = materialState.frontface; if (frameCtx.frontface !== frontface) { if (frontface) { gl.frontFace(gl.CCW); } else { gl.frontFace(gl.CW); } frameCtx.frontface = frontface; } if (frameCtx.lineWidth !== materialState.lineWidth) { gl.lineWidth(materialState.lineWidth); frameCtx.lineWidth = materialState.lineWidth; } if (this._uPointSize) { gl.uniform1f(this._uPointSize, materialState.pointSize); } switch (materialState.type) { case "LambertMaterial": if (this._uMaterialAmbient) { gl.uniform3fv(this._uMaterialAmbient, materialState.ambient); } if (this._uMaterialColor) { gl.uniform4f(this._uMaterialColor, materialState.color[0], materialState.color[1], materialState.color[2], materialState.alpha); } if (this._uMaterialEmissive) { gl.uniform3fv(this._uMaterialEmissive, materialState.emissive); } break; case "PhongMaterial": if (this._uMaterialShininess) { gl.uniform1f(this._uMaterialShininess, materialState.shininess); } if (this._uMaterialAmbient) { gl.uniform3fv(this._uMaterialAmbient, materialState.ambient); } if (this._uMaterialDiffuse) { gl.uniform3fv(this._uMaterialDiffuse, materialState.diffuse); } if (this._uMaterialSpecular) { gl.uniform3fv(this._uMaterialSpecular, materialState.specular); } if (this._uMaterialEmissive) { gl.uniform3fv(this._uMaterialEmissive, materialState.emissive); } if (this._uAlphaModeCutoff) { gl.uniform4f( this._uAlphaModeCutoff, 1.0 * materialState.alpha, materialState.alphaMode === 1 ? 1.0 : 0.0, materialState.alphaCutoff, 0); } if (material._ambientMap && material._ambientMap._state.texture && this._uMaterialAmbientMap) { program.bindTexture(this._uMaterialAmbientMap, material._ambientMap._state.texture, frameCtx.textureUnit); frameCtx.textureUnit = (frameCtx.textureUnit + 1) % maxTextureUnits; frameCtx.bindTexture++; if (this._uMaterialAmbientMapMatrix) { gl.uniformMatrix4fv(this._uMaterialAmbientMapMatrix, false, material._ambientMap._state.matrix); } } if (material._diffuseMap && material._diffuseMap._state.texture && this._uDiffuseMap) { program.bindTexture(this._uDiffuseMap, material._diffuseMap._state.texture, frameCtx.textureUnit); frameCtx.textureUnit = (frameCtx.textureUnit + 1) % maxTextureUnits; frameCtx.bindTexture++; if (this._uDiffuseMapMatrix) { gl.uniformMatrix4fv(this._uDiffuseMapMatrix, false, material._diffuseMap._state.matrix); } } if (material._specularMap && material._specularMap._state.texture && this._uSpecularMap) { program.bindTexture(this._uSpecularMap, material._specularMap._state.texture, frameCtx.textureUnit); frameCtx.textureUnit = (frameCtx.textureUnit + 1) % maxTextureUnits; frameCtx.bindTexture++; if (this._uSpecularMapMatrix) { gl.uniformMatrix4fv(this._uSpecularMapMatrix, false, material._specularMap._state.matrix); } } if (material._emissiveMap && material._emissiveMap._state.texture && this._uEmissiveMap) { program.bindTexture(this._uEmissiveMap, material._emissiveMap._state.texture, frameCtx.textureUnit); frameCtx.textureUnit = (frameCtx.textureUnit + 1) % maxTextureUnits; frameCtx.bindTexture++; if (this._uEmissiveMapMatrix) { gl.uniformMatrix4fv(this._uEmissiveMapMatrix, false, material._emissiveMap._state.matrix); } } if (material._alphaMap && material._alphaMap._state.texture && this._uAlphaMap) { program.bindTexture(this._uAlphaMap, material._alphaMap._state.texture, frameCtx.textureUnit); frameCtx.textureUnit = (frameCtx.textureUnit + 1) % maxTextureUnits; frameCtx.bindTexture++; if (this._uAlphaMapMatrix) { gl.uniformMatrix4fv(this._uAlphaMapMatrix, false, material._alphaMap._state.matrix); } } if (material._reflectivityMap && material._reflectivityMap._state.texture && this._uReflectivityMap) { program.bindTexture(this._uReflectivityMap, material._reflectivityMap._state.texture, frameCtx.textureUnit); frameCtx.textureUnit = (frameCtx.textureUnit + 1) % maxTextureUnits; if (this._uReflectivityMapMatrix) { gl.uniformMatrix4fv(this._uReflectivityMapMatrix, false, material._reflectivityMap._state.matrix); } } if (material._normalMap && material._normalMap._state.texture && this._uNormalMap) { program.bindTexture(this._uNormalMap, material._normalMap._state.texture, frameCtx.textureUnit); frameCtx.textureUnit = (frameCtx.textureUnit + 1) % maxTextureUnits; frameCtx.bindTexture++; if (this._uNormalMapMatrix) { gl.uniformMatrix4fv(this._uNormalMapMatrix, false, material._normalMap._state.matrix); } } if (material._occlusionMap && material._occlusionMap._state.texture && this._uOcclusionMap) { program.bindTexture(this._uOcclusionMap, material._occlusionMap._state.texture, frameCtx.textureUnit); frameCtx.textureUnit = (frameCtx.textureUnit + 1) % maxTextureUnits; frameCtx.bindTexture++; if (this._uOcclusionMapMatrix) { gl.uniformMatrix4fv(this._uOcclusionMapMatrix, false, material._occlusionMap._state.matrix); } } if (material._diffuseFresnel) { if (this._uDiffuseFresnelEdgeBias) { gl.uniform1f(this._uDiffuseFresnelEdgeBias, material._diffuseFresnel.edgeBias); } if (this._uDiffuseFresnelCenterBias) { gl.uniform1f(this._uDiffuseFresnelCenterBias, material._diffuseFresnel.centerBias); } if (this._uDiffuseFresnelEdgeColor) { gl.uniform3fv(this._uDiffuseFresnelEdgeColor, material._diffuseFresnel.edgeColor); } if (this._uDiffuseFresnelCenterColor) { gl.uniform3fv(this._uDiffuseFresnelCenterColor, material._diffuseFresnel.centerColor); } if (this._uDiffuseFresnelPower) { gl.uniform1f(this._uDiffuseFresnelPower, material._diffuseFresnel.power); } } if (material._specularFresnel) { if (this._uSpecularFresnelEdgeBias) { gl.uniform1f(this._uSpecularFresnelEdgeBias, material._specularFresnel.edgeBias); } if (this._uSpecularFresnelCenterBias) { gl.uniform1f(this._uSpecularFresnelCenterBias, material._specularFresnel.centerBias); } if (this._uSpecularFresnelEdgeColor) { gl.uniform3fv(this._uSpecularFresnelEdgeColor, material._specularFresnel.edgeColor); } if (this._uSpecularFresnelCenterColor) { gl.uniform3fv(this._uSpecularFresnelCenterColor, material._specularFresnel.centerColor); } if (this._uSpecularFresnelPower) { gl.uniform1f(this._uSpecularFresnelPower, material._specularFresnel.power); } } if (material._alphaFresnel) { if (this._uAlphaFresnelEdgeBias) { gl.uniform1f(this._uAlphaFresnelEdgeBias, material._alphaFresnel.edgeBias); } if (this._uAlphaFresnelCenterBias) { gl.uniform1f(this._uAlphaFresnelCenterBias, material._alphaFresnel.centerBias); } if (this._uAlphaFresnelEdgeColor) { gl.uniform3fv(this._uAlphaFresnelEdgeColor, material._alphaFresnel.edgeColor); } if (this._uAlphaFresnelCenterColor) { gl.uniform3fv(this._uAlphaFresnelCenterColor, material._alphaFresnel.centerColor); } if (this._uAlphaFresnelPower) { gl.uniform1f(this._uAlphaFresnelPower, material._alphaFresnel.power); } } if (material._reflectivityFresnel) { if (this._uReflectivityFresnelEdgeBias) { gl.uniform1f(this._uReflectivityFresnelEdgeBias, material._reflectivityFresnel.edgeBias); } if (this._uReflectivityFresnelCenterBias) { gl.uniform1f(this._uReflectivityFresnelCenterBias, material._reflectivityFresnel.centerBias); } if (this._uReflectivityFresnelEdgeColor) { gl.uniform3fv(this._uReflectivityFresnelEdgeColor, material._reflectivityFresnel.edgeColor); } if (this._uReflectivityFresnelCenterColor) { gl.uniform3fv(this._uReflectivityFresnelCenterColor, material._reflectivityFresnel.centerColor); } if (this._uReflectivityFresnelPower) { gl.uniform1f(this._uReflectivityFresnelPower, material._reflectivityFresnel.power); } } if (material._emissiveFresnel) { if (this._uEmissiveFresnelEdgeBias) { gl.uniform1f(this._uEmissiveFresnelEdgeBias, material._emissiveFresnel.edgeBias); } if (this._uEmissiveFresnelCenterBias) { gl.uniform1f(this._uEmissiveFresnelCenterBias, material._emissiveFresnel.centerBias); } if (this._uEmissiveFresnelEdgeColor) { gl.uniform3fv(this._uEmissiveFresnelEdgeColor, material._emissiveFresnel.edgeColor); } if (this._uEmissiveFresnelCenterColor) { gl.uniform3fv(this._uEmissiveFresnelCenterColor, material._emissiveFresnel.centerColor); } if (this._uEmissiveFresnelPower) { gl.uniform1f(this._uEmissiveFresnelPower, material._emissiveFresnel.power); } } break; case "MetallicMaterial": if (this._uBaseColor) { gl.uniform3fv(this._uBaseColor, materialState.baseColor); } if (this._uMaterialMetallic) { gl.uniform1f(this._uMaterialMetallic, materialState.metallic); } if (this._uMaterialRoughness) { gl.uniform1f(this._uMaterialRoughness, materialState.roughness); } if (this._uMaterialSpecularF0) { gl.uniform1f(this._uMaterialSpecularF0, materialState.specularF0); } if (this._uMaterialEmissive) { gl.uniform3fv(this._uMaterialEmissive, materialState.emissive); } if (this._uAlphaModeCutoff) { gl.uniform4f( this._uAlphaModeCutoff, 1.0 * materialState.alpha, materialState.alphaMode === 1 ? 1.0 : 0.0, materialState.alphaCutoff, 0.0); } const baseColorMap = material._baseColorMap; if (baseColorMap && baseColorMap._state.texture && this._uBaseColorMap) { program.bindTexture(this._uBaseColorMap, baseColorMap._state.texture, frameCtx.textureUnit); frameCtx.textureUnit = (frameCtx.textureUnit + 1) % maxTextureUnits; frameCtx.bindTexture++; if (this._uBaseColorMapMatrix) { gl.uniformMatrix4fv(this._uBaseColorMapMatrix, false, baseColorMap._state.matrix); } } const metallicMap = material._metallicMap; if (metallicMap && metallicMap._state.texture && this._uMetallicMap) { program.bindTexture(this._uMetallicMap, metallicMap._state.texture, frameCtx.textureUnit); frameCtx.textureUnit = (frameCtx.textureUnit + 1) % maxTextureUnits; frameCtx.bindTexture++; if (this._uMetallicMapMatrix) { gl.uniformMatrix4fv(this._uMetallicMapMatrix, false, metallicMap._state.matrix); } } const roughnessMap = material._roughnessMap; if (roughnessMap && roughnessMap._state.texture && this._uRoughnessMap) { program.bindTexture(this._uRoughnessMap, roughnessMap._state.texture, frameCtx.textureUnit); frameCtx.textureUnit = (frameCtx.textureUnit + 1) % maxTextureUnits; frameCtx.bindTexture++; if (this._uRoughnessMapMatrix) { gl.uniformMatrix4fv(this._uRoughnessMapMatrix, false, roughnessMap._state.matrix); } } const metallicRoughnessMap = material._metallicRoughnessMap; if (metallicRoughnessMap && metallicRoughnessMap._state.texture && this._uMetallicRoughnessMap) { program.bindTexture(this._uMetallicRoughnessMap, metallicRoughnessMap._state.texture, frameCtx.textureUnit); frameCtx.textureUnit = (frameCtx.textureUnit + 1) % maxTextureUnits; frameCtx.bindTexture++; if (this._uMetallicRoughnessMapMatrix) { gl.uniformMatrix4fv(this._uMetallicRoughnessMapMatrix, false, metallicRoughnessMap._state.matrix); } } var emissiveMap = material._emissiveMap; if (emissiveMap && emissiveMap._state.texture && this._uEmissiveMap) { program.bindTexture(this._uEmissiveMap, emissiveMap._state.texture, frameCtx.textureUnit); frameCtx.textureUnit = (frameCtx.textureUnit + 1) % maxTextureUnits; frameCtx.bindTexture++; if (this._uEmissiveMapMatrix) { gl.uniformMatrix4fv(this._uEmissiveMapMatrix, false, emissiveMap._state.matrix); } } var occlusionMap = material._occlusionMap; if (occlusionMap && material._occlusionMap._state.texture && this._uOcclusionMap) { program.bindTexture(this._uOcclusionMap, occlusionMap._state.texture, frameCtx.textureUnit); frameCtx.textureUnit = (frameCtx.textureUnit + 1) % maxTextureUnits; frameCtx.bindTexture++; if (this._uOcclusionMapMatrix) { gl.uniformMatrix4fv(this._uOcclusionMapMatrix, false, occlusionMap._state.matrix); } } var alphaMap = material._alphaMap; if (alphaMap && alphaMap._state.texture && this._uAlphaMap) { program.bindTexture(this._uAlphaMap, alphaMap._state.texture, frameCtx.textureUnit); frameCtx.textureUnit = (frameCtx.textureUnit + 1) % maxTextureUnits; frameCtx.bindTexture++; if (this._uAlphaMapMatrix) { gl.uniformMatrix4fv(this._uAlphaMapMatrix, false, alphaMap._state.matrix); } } var normalMap = material._normalMap; if (normalMap && normalMap._state.texture && this._uNormalMap) { program.bindTexture(this._uNormalMap, normalMap._state.texture, frameCtx.textureUnit); frameCtx.textureUnit = (frameCtx.textureUnit + 1) % maxTextureUnits; frameCtx.bindTexture++; if (this._uNormalMapMatrix) { gl.uniformMatrix4fv(this._uNormalMapMatrix, false, normalMap._state.matrix); } } break; case "SpecularMaterial": if (this._uMaterialDiffuse) { gl.uniform3fv(this._uMaterialDiffuse, materialState.diffuse); } if (this._uMaterialSpecular) { gl.uniform3fv(this._uMaterialSpecular, materialState.specular); } if (this._uMaterialGlossiness) { gl.uniform1f(this._uMaterialGlossiness, materialState.glossiness); } if (this._uMaterialReflectivity) { gl.uniform1f(this._uMaterialReflectivity, materialState.reflectivity); } if (this._uMaterialEmissive) { gl.uniform3fv(this._uMaterialEmissive, materialState.emissive); } if (this._uAlphaModeCutoff) { gl.uniform4f( this._uAlphaModeCutoff, 1.0 * materialState.alpha, materialState.alphaMode === 1 ? 1.0 : 0.0, materialState.alphaCutoff, 0.0); } const diffuseMap = material._diffuseMap; if (diffuseMap && diffuseMap._state.texture && this._uDiffuseMap) { program.bindTexture(this._uDiffuseMap, diffuseMap._state.texture, frameCtx.textureUnit); frameCtx.textureUnit = (frameCtx.textureUnit + 1) % maxTextureUnits; frameCtx.bindTexture++; if (this._uDiffuseMapMatrix) { gl.uniformMatrix4fv(this._uDiffuseMapMatrix, false, diffuseMap._state.matrix); } } const specularMap = material._specularMap; if (specularMap && specularMap._state.texture && this._uSpecularMap) { program.bindTexture(this._uSpecularMap, specularMap._state.texture, frameCtx.textureUnit); frameCtx.textureUnit = (frameCtx.textureUnit + 1) % maxTextureUnits; frameCtx.bindTexture++; if (this._uSpecularMapMatrix) { gl.uniformMatrix4fv(this._uSpecularMapMatrix, false, specularMap._state.matrix); } } const glossinessMap = material._glossinessMap; if (glossinessMap && glossinessMap._state.texture && this._uGlossinessMap) { program.bindTexture(this._uGlossinessMap, glossinessMap._state.texture, frameCtx.textureUnit); frameCtx.textureUnit = (frameCtx.textureUnit + 1) % maxTextureUnits; frameCtx.bindTexture++; if (this._uGlossinessMapMatrix) { gl.uniformMatrix4fv(this._uGlossinessMapMatrix, false, glossinessMap._state.matrix); } } const specularGlossinessMap = material._specularGlossinessMap; if (specularGlossinessMap && specularGlossinessMap._state.texture && this._uSpecularGlossinessMap) { program.bindTexture(this._uSpecularGlossinessMap, specularGlossinessMap._state.texture, frameCtx.textureUnit); frameCtx.textureUnit = (frameCtx.textureUnit + 1) % maxTextureUnits; frameCtx.bindTexture++; if (this._uSpecularGlossinessMapMatrix) { gl.uniformMatrix4fv(this._uSpecularGlossinessMapMatrix, false, specularGlossinessMap._state.matrix); } } var emissiveMap = material._emissiveMap; if (emissiveMap && emissiveMap._state.texture && this._uEmissiveMap) { program.bindTexture(this._uEmissiveMap, emissiveMap._state.texture, frameCtx.textureUnit); frameCtx.textureUnit = (frameCtx.textureUnit + 1) % maxTextureUnits; frameCtx.bindTexture++; if (this._uEmissiveMapMatrix) { gl.uniformMatrix4fv(this._uEmissiveMapMatrix, false, emissiveMap._state.matrix); } } var occlusionMap = material._occlusionMap; if (occlusionMap && occlusionMap._state.texture && this._uOcclusionMap) { program.bindTexture(this._uOcclusionMap, occlusionMap._state.texture, frameCtx.textureUnit); frameCtx.textureUnit = (frameCtx.textureUnit + 1) % maxTextureUnits; frameCtx.bindTexture++; if (this._uOcclusionMapMatrix) { gl.uniformMatrix4fv(this._uOcclusionMapMatrix, false, occlusionMap._state.matrix); } } var alphaMap = material._alphaMap; if (alphaMap && alphaMap._state.texture && this._uAlphaMap) { program.bindTexture(this._uAlphaMap, alphaMap._state.texture, frameCtx.textureUnit); frameCtx.textureUnit = (frameCtx.textureUnit + 1) % maxTextureUnits; frameCtx.bindTexture++; if (this._uAlphaMapMatrix) { gl.uniformMatrix4fv(this._uAlphaMapMatrix, false, alphaMap._state.matrix); } } var normalMap = material._normalMap; if (normalMap && normalMap._state.texture && this._uNormalMap) { program.bindTexture(this._uNormalMap, normalMap._state.texture, frameCtx.textureUnit); frameCtx.textureUnit = (frameCtx.textureUnit + 1) % maxTextureUnits; frameCtx.bindTexture++; if (this._uNormalMapMatrix) { gl.uniformMatrix4fv(this._uNormalMapMatrix, false, normalMap._state.matrix); } } break; } this._lastMaterialId = materialState.id; } gl.uniformMatrix4fv(this._uModelMatrix, gl.FALSE, mesh.worldMatrix); if (this._uModelNormalMatrix) { gl.uniformMatrix4fv(this._uModelNormalMatrix, gl.FALSE, mesh.worldNormalMatrix); } if (this._uClippable) { gl.uniform1i(this._uClippable, meshState.clippable); } if (this._uColorize) { const colorize = meshState.colorize; const lastColorize = this._lastColorize; if (lastColorize[0] !== colorize[0] || lastColorize[1] !== colorize[1] || lastColorize[2] !== colorize[2] || lastColorize[3] !== colorize[3]) { gl.uniform4fv(this._uColorize, colorize); lastColorize[0] = colorize[0]; lastColorize[1] = colorize[1]; lastColorize[2] = colorize[2]; lastColorize[3] = colorize[3]; } } gl.uniform3fv(this._uOffset, meshState.offset); // Bind VBOs if (geometryState.id !== this._lastGeometryId) { if (this._uPositionsDecodeMatrix) { gl.uniformMatrix4fv(this._uPositionsDecodeMatrix, false, geometryState.positionsDecodeMatrix); } if (this._uUVDecodeMatrix) { gl.uniformMatrix3fv(this._uUVDecodeMatrix, false, geometryState.uvDecodeMatrix); } if (this._aPosition) { this._aPosition.bindArrayBuffer(geometryState.positionsBuf); frameCtx.bindArray++; } if (this._aNormal) { this._aNormal.bindArrayBuffer(geometryState.normalsBuf); frameCtx.bindArray++; } if (this._aUV) { this._aUV.bindArrayBuffer(geometryState.uvBuf); frameCtx.bindArray++; } if (this._aColor) { this._aColor.bindArrayBuffer(geometryState.colorsBuf); frameCtx.bindArray++; } if (this._aFlags) { this._aFlags.bindArrayBuffer(geometryState.flagsBuf); frameCtx.bindArray++; } if (geometryState.indicesBuf) { geometryState.indicesBuf.bind(); frameCtx.bindArray++; } this._lastGeometryId = geometryState.id; } // Draw (indices bound in prev step) if (geometryState.indicesBuf) { gl.drawElements(geometryState.primitive, geometryState.indicesBuf.numItems, geometryState.indicesBuf.itemType, 0); frameCtx.drawElements++; } else if (geometryState.positions) { gl.drawArrays(gl.TRIANGLES, 0, geometryState.positions.numItems); frameCtx.drawArrays++; } if (background) { gl.depthFunc(gl.LESS); } }; DrawRenderer.prototype._allocate = function (mesh) { const scene = mesh.scene; const gl = scene.canvas.gl; const material = mesh._material; const lightsState = scene._lightsState; const sectionPlanesState = scene._sectionPlanesState; const materialState = mesh._material._state; this._program = new Program(gl, this._shaderSource); if (this._program.errors) { this.errors = this._program.errors; return; } const program = this._program; this._uPositionsDecodeMatrix = program.getLocation("positionsDecodeMatrix"); this._uUVDecodeMatrix = program.getLocation("uvDecodeMatrix"); this._uModelMatrix = program.getLocation("modelMatrix"); this._uModelNormalMatrix = program.getLocation("modelNormalMatrix"); this._uViewMatrix = program.getLocation("viewMatrix"); this._uViewNormalMatrix = program.getLocation("viewNormalMatrix"); this._uProjMatrix = program.getLocation("projMatrix"); this._uGammaFactor = program.getLocation("gammaFactor"); this._uLightAmbient = []; this._uLightColor = []; this._uLightDir = []; this._uLightPos = []; this._uLightAttenuation = []; this._uShadowViewMatrix = []; this._uShadowProjMatrix = []; if (scene.logarithmicDepthBufferEnabled) { this._uLogDepthBufFC = program.getLocation("logDepthBufFC"); } const lights = lightsState.lights; let light; for (var i = 0, len = lights.length; i < len; i++) { light = lights[i]; switch (light.type) { case "ambient": this._uLightAmbient[i] = program.getLocation("lightAmbient"); break; case "dir": this._uLightColor[i] = program.getLocation("lightColor" + i); this._uLightPos[i] = null; this._uLightDir[i] = program.getLocation("lightDir" + i); break; case "point": this._uLightColor[i] = program.getLocation("lightColor" + i); this._uLightPos[i] = program.getLocation("lightPos" + i); this._uLightDir[i] = null; this._uLightAttenuation[i] = program.getLocation("lightAttenuation" + i); break; case "spot": this._uLightColor[i] = program.getLocation("lightColor" + i); this._uLightPos[i] = program.getLocation("lightPos" + i); this._uLightDir[i] = program.getLocation("lightDir" + i); this._uLightAttenuation[i] = program.getLocation("lightAttenuation" + i); break; } if (light.castsShadow) { this._uShadowViewMatrix[i] = program.getLocation("shadowViewMatrix" + i); this._uShadowProjMatrix[i] = program.getLocation("shadowProjMatrix" + i); } } if (lightsState.lightMaps.length > 0) { this._uLightMap = "lightMap"; } if (lightsState.reflectionMaps.length > 0) { this._uReflectionMap = "reflectionMap"; } this._uSectionPlanes = []; const sectionPlanes = sectionPlanesState.sectionPlanes; for (var i = 0, len = sectionPlanes.length; i < len; i++) { this._uSectionPlanes.push({ active: program.getLocation("sectionPlaneActive" + i), pos: program.getLocation("sectionPlanePos" + i), dir: program.getLocation("sectionPlaneDir" + i) }); } this._uPointSize = program.getLocation("pointSize"); switch (materialState.type) { case "LambertMaterial": this._uMaterialColor = program.getLocation("materialColor"); this._uMaterialEmissive = program.getLocation("materialEmissive"); this._uAlphaModeCutoff = program.getLocation("materialAlphaModeCutoff"); break; case "PhongMaterial": this._uMaterialAmbient = program.getLocation("materialAmbient"); this._uMaterialDiffuse = program.getLocation("materialDiffuse"); this._uMaterialSpecular = program.getLocation("materialSpecular"); this._uMaterialEmissive = program.getLocation("materialEmissive"); this._uAlphaModeCutoff = program.getLocation("materialAlphaModeCutoff"); this._uMaterialShininess = program.getLocation("materialShininess"); if (material._ambientMap) { this._uMaterialAmbientMap = "ambientMap"; this._uMaterialAmbientMapMatrix = program.getLocation("ambientMapMatrix"); } if (material._diffuseMap) { this._uDiffuseMap = "diffuseMap"; this._uDiffuseMapMatrix = program.getLocation("diffuseMapMatrix"); } if (material._specularMap) { this._uSpecularMap = "specularMap"; this._uSpecularMapMatrix = program.getLocation("specularMapMatrix"); } if (material._emissiveMap) { this._uEmissiveMap = "emissiveMap"; this._uEmissiveMapMatrix = program.getLocation("emissiveMapMatrix"); } if (material._alphaMap) { this._uAlphaMap = "alphaMap"; this._uAlphaMapMatrix = program.getLocation("alphaMapMatrix"); } if (material._reflectivityMap) { this._uReflectivityMap = "reflectivityMap"; this._uReflectivityMapMatrix = program.getLocation("reflectivityMapMatrix"); } if (material._normalMap) { this._uNormalMap = "normalMap"; this._uNormalMapMatrix = program.getLocation("normalMapMatrix"); } if (material._occlusionMap) { this._uOcclusionMap = "occlusionMap"; this._uOcclusionMapMatrix = program.getLocation("occlusionMapMatrix"); } if (material._diffuseFresnel) { this._uDiffuseFresnelEdgeBias = program.getLocation("diffuseFresnelEdgeBias"); this._uDiffuseFresnelCenterBias = program.getLocation("diffuseFresnelCenterBias"); this._uDiffuseFresnelEdgeColor = program.getLocation("diffuseFresnelEdgeColor"); this._uDiffuseFresnelCenterColor = program.getLocation("diffuseFresnelCenterColor"); this._uDiffuseFresnelPower = program.getLocation("diffuseFresnelPower"); } if (material._specularFresnel) { this._uSpecularFresnelEdgeBias = program.getLocation("specularFresnelEdgeBias"); this._uSpecularFresnelCenterBias = program.getLocation("specularFresnelCenterBias"); this._uSpecularFresnelEdgeColor = program.getLocation("specularFresnelEdgeColor"); this._uSpecularFresnelCenterColor = program.getLocation("specularFresnelCenterColor"); this._uSpecularFresnelPower = program.getLocation("specularFresnelPower"); } if (material._alphaFresnel) { this._uAlphaFresnelEdgeBias = program.getLocation("alphaFresnelEdgeBias"); this._uAlphaFresnelCenterBias = program.getLocation("alphaFresnelCenterBias"); this._uAlphaFresnelEdgeColor = program.getLocation("alphaFresnelEdgeColor"); this._uAlphaFresnelCenterColor = program.getLocation("alphaFresnelCenterColor"); this._uAlphaFresnelPower = program.getLocation("alphaFresnelPower"); } if (material._reflectivityFresnel) { this._uReflectivityFresnelEdgeBias = program.getLocation("reflectivityFresnelEdgeBias"); this._uReflectivityFresnelCenterBias = program.getLocation("reflectivityFresnelCenterBias"); this._uReflectivityFresnelEdgeColor = program.getLocation("reflectivityFresnelEdgeColor"); this._uReflectivityFresnelCenterColor = program.getLocation("reflectivityFresnelCenterColor"); this._uReflectivityFresnelPower = program.getLocation("reflectivityFresnelPower"); } if (material._emissiveFresnel) { this._uEmissiveFresnelEdgeBias = program.getLocation("emissiveFresnelEdgeBias"); this._uEmissiveFresnelCenterBias = program.getLocation("emissiveFresnelCenterBias"); this._uEmissiveFresnelEdgeColor = program.getLocation("emissiveFresnelEdgeColor"); this._uEmissiveFresnelCenterColor = program.getLocation("emissiveFresnelCenterColor"); this._uEmissiveFresnelPower = program.getLocation("emissiveFresnelPower"); } break; case "MetallicMaterial": this._uBaseColor = program.getLocation("materialBaseColor"); this._uMaterialMetallic = program.getLocation("materialMetallic"); this._uMaterialRoughness = program.getLocation("materialRoughness"); this._uMaterialSpecularF0 = program.getLocation("materialSpecularF0"); this._uMaterialEmissive = program.getLocation("materialEmissive"); this._uAlphaModeCutoff = program.getLocation("materialAlphaModeCutoff"); if (material._baseColorMap) { this._uBaseColorMap = "baseColorMap"; this._uBaseColorMapMatrix = program.getLocation("baseColorMapMatrix"); } if (material._metallicMap) { this._uMetallicMap = "metallicMap"; this._uMetallicMapMatrix = program.getLocation("metallicMapMatrix"); } if (material._roughnessMap) { this._uRoughnessMap = "roughnessMap"; this._uRoughnessMapMatrix = program.getLocation("roughnessMapMatrix"); } if (material._metallicRoughnessMap) { this._uMetallicRoughnessMap = "metallicRoughnessMap"; this._uMetallicRoughnessMapMatrix = program.getLocation("metallicRoughnessMapMatrix"); } if (material._emissiveMap) { this._uEmissiveMap = "emissiveMap"; this._uEmissiveMapMatrix = program.getLocation("emissiveMapMatrix"); } if (material._occlusionMap) { this._uOcclusionMap = "occlusionMap"; this._uOcclusionMapMatrix = program.getLocation("occlusionMapMatrix"); } if (material._alphaMap) { this._uAlphaMap = "alphaMap"; this._uAlphaMapMatrix = program.getLocation("alphaMapMatrix"); } if (material._normalMap) { this._uNormalMap = "normalMap"; this._uNormalMapMatrix = program.getLocation("normalMapMatrix"); } break; case "SpecularMaterial": this._uMaterialDiffuse = program.getLocation("materialDiffuse"); this._uMaterialSpecular = program.getLocation("materialSpecular"); this._uMaterialGlossiness = program.getLocation("materialGlossiness"); this._uMaterialReflectivity = program.getLocation("reflectivityFresnel"); this._uMaterialEmissive = program.getLocation("materialEmissive"); this._uAlphaModeCutoff = program.getLocation("materialAlphaModeCutoff"); if (material._diffuseMap) { this._uDiffuseMap = "diffuseMap"; this._uDiffuseMapMatrix = program.getLocation("diffuseMapMatrix"); } if (material._specularMap) { this._uSpecularMap = "specularMap"; this._uSpecularMapMatrix = program.getLocation("specularMapMatrix"); } if (material._glossinessMap) { this._uGlossinessMap = "glossinessMap"; this._uGlossinessMapMatrix = program.getLocation("glossinessMapMatrix"); } if (material._specularGlossinessMap) { this._uSpecularGlossinessMap = "materialSpecularGlossinessMap"; this._uSpecularGlossinessMapMatrix = program.getLocation("materialSpecularGlossinessMapMatrix"); } if (material._emissiveMap) { this._uEmissiveMap = "emissiveMap"; this._uEmissiveMapMatrix = program.getLocation("emissiveMapMatrix"); } if (material._occlusionMap) { this._uOcclusionMap = "occlusionMap"; this._uOcclusionMapMatrix = program.getLocation("occlusionMapMatrix"); } if (material._alphaMap) { this._uAlphaMap = "alphaMap"; this._uAlphaMapMatrix = program.getLocation("alphaMapMatrix"); } if (material._normalMap) { this._uNormalMap = "normalMap"; this._uNormalMapMatrix = program.getLocation("normalMapMatrix"); } break; } this._aPosition = program.getAttribute("position"); this._aNormal = program.getAttribute("normal"); this._aUV = program.getAttribute("uv"); this._aColor = program.getAttribute("color"); this._aFlags = program.getAttribute("flags"); this._uClippable = program.getLocation("clippable"); this._uColorize = program.getLocation("colorize"); this._uOffset = program.getLocation("offset"); this._lastMaterialId = null; this._lastVertexBufsId = null; this._lastGeometryId = null; this._lastColorize = new Float32Array(4); this._baseTextureUnit = 0; }; DrawRenderer.prototype._bindProgram = function (frameCtx) { const maxTextureUnits = WEBGL_INFO.MAX_TEXTURE_UNITS; const scene = this._scene; const gl = scene.canvas.gl; const lightsState = scene._lightsState; const project = scene.camera.project; let light; const program = this._program; program.bind(); frameCtx.useProgram++; frameCtx.textureUnit = 0; this._lastMaterialId = null; this._lastVertexBufsId = null; this._lastGeometryId = null; this._lastColorize[0] = -1; this._lastColorize[1] = -1; this._lastColorize[2] = -1; this._lastColorize[3] = -1; gl.uniformMatrix4fv(this._uProjMatrix, false, project.matrix); if (scene.logarithmicDepthBufferEnabled) { const logDepthBufFC = 2.0 / (Math.log(project.far + 1.0) / Math.LN2); gl.uniform1f(this._uLogDepthBufFC, logDepthBufFC); } for (var i = 0, len = lightsState.lights.length; i < len; i++) { light = lightsState.lights[i]; if (this._uLightAmbient[i]) { gl.uniform4f(this._uLightAmbient[i], light.color[0], light.color[1], light.color[2], light.intensity); } else { if (this._uLightColor[i]) { gl.uniform4f(this._uLightColor[i], light.color[0], light.color[1], light.color[2], light.intensity); } if (this._uLightPos[i]) { gl.uniform3fv(this._uLightPos[i], light.pos); if (this._uLightAttenuation[i]) { gl.uniform1f(this._uLightAttenuation[i], light.attenuation); } } if (this._uLightDir[i]) { gl.uniform3fv(this._uLightDir[i], light.dir); } if (light.castsShadow) { if (this._uShadowViewMatrix[i]) { gl.uniformMatrix4fv(this._uShadowViewMatrix[i], false, light.getShadowViewMatrix()); } if (this._uShadowProjMatrix[i]) { gl.uniformMatrix4fv(this._uShadowProjMatrix[i], false, light.getShadowProjMatrix()); } const shadowRenderBuf = light.getShadowRenderBuf(); if (shadowRenderBuf) { program.bindTexture("shadowMap" + i, shadowRenderBuf.getTexture(), frameCtx.textureUnit); frameCtx.textureUnit = (frameCtx.textureUnit + 1) % maxTextureUnits; frameCtx.bindTexture++; } } } } if (lightsState.lightMaps.length > 0 && lightsState.lightMaps[0].texture && this._uLightMap) { program.bindTexture(this._uLightMap, lightsState.lightMaps[0].texture, frameCtx.textureUnit); frameCtx.textureUnit = (frameCtx.textureUnit + 1) % maxTextureUnits; frameCtx.bindTexture++; } if (lightsState.reflectionMaps.length > 0 && lightsState.reflectionMaps[0].texture && this._uReflectionMap) { program.bindTexture(this._uReflectionMap, lightsState.reflectionMaps[0].texture, frameCtx.textureUnit); frameCtx.textureUnit = (frameCtx.textureUnit + 1) % maxTextureUnits; frameCtx.bindTexture++; } if (this._uGammaFactor) { gl.uniform1f(this._uGammaFactor, scene.gammaFactor); } this._baseTextureUnit = frameCtx.textureUnit; }; /** * @private */ class EmphasisFillShaderSource { constructor(mesh) { this.vertex = buildVertex$5(mesh); this.fragment = buildFragment$5(mesh); } } function buildVertex$5(mesh) { const scene = mesh.scene; const lightsState = scene._lightsState; const normals = hasNormals(mesh); const sectionPlanesState = scene._sectionPlanesState; const clipping = sectionPlanesState.getNumAllocatedSectionPlanes() > 0; const quantizedGeometry = !!mesh._geometry._state.compressGeometry; const billboard = mesh._state.billboard; const stationary = mesh._state.stationary; const src = []; src.push("#version 300 es"); src.push("// EmphasisFillShaderSource vertex shader"); src.push("in vec3 position;"); src.push("uniform mat4 modelMatrix;"); src.push("uniform mat4 viewMatrix;"); src.push("uniform mat4 projMatrix;"); src.push("uniform vec4 colorize;"); src.push("uniform vec3 offset;"); if (quantizedGeometry) { src.push("uniform mat4 positionsDecodeMatrix;"); } if (scene.logarithmicDepthBufferEnabled) { src.push("uniform float logDepthBufFC;"); src.push("out float vFragDepth;"); src.push("bool isPerspectiveMatrix(mat4 m) {"); src.push(" return (m[2][3] == - 1.0);"); src.push("}"); src.push("out float isPerspective;"); } if (clipping) { src.push("out vec4 vWorldPosition;"); } src.push("uniform vec4 lightAmbient;"); src.push("uniform vec4 fillColor;"); if (normals) { src.push("in vec3 normal;"); src.push("uniform mat4 modelNormalMatrix;"); src.push("uniform mat4 viewNormalMatrix;"); for (let i = 0, len = lightsState.lights.length; i < len; i++) { const light = lightsState.lights[i]; if (light.type === "ambient") { continue; } src.push("uniform vec4 lightColor" + i + ";"); if (light.type === "dir") { src.push("uniform vec3 lightDir" + i + ";"); } if (light.type === "point") { src.push("uniform vec3 lightPos" + i + ";"); } if (light.type === "spot") { src.push("uniform vec3 lightPos" + i + ";"); } } if (quantizedGeometry) { src.push("vec3 octDecode(vec2 oct) {"); src.push(" vec3 v = vec3(oct.xy, 1.0 - abs(oct.x) - abs(oct.y));"); src.push(" if (v.z < 0.0) {"); src.push(" v.xy = (1.0 - abs(v.yx)) * vec2(v.x >= 0.0 ? 1.0 : -1.0, v.y >= 0.0 ? 1.0 : -1.0);"); src.push(" }"); src.push(" return normalize(v);"); src.push("}"); } } src.push("out vec4 vColor;"); if (billboard === "spherical" || billboard === "cylindrical") { src.push("void billboard(inout mat4 mat) {"); src.push(" mat[0][0] = 1.0;"); src.push(" mat[0][1] = 0.0;"); src.push(" mat[0][2] = 0.0;"); if (billboard === "spherical") { src.push(" mat[1][0] = 0.0;"); src.push(" mat[1][1] = 1.0;"); src.push(" mat[1][2] = 0.0;"); } src.push(" mat[2][0] = 0.0;"); src.push(" mat[2][1] = 0.0;"); src.push(" mat[2][2] =1.0;"); src.push("}"); } src.push("void main(void) {"); src.push("vec4 localPosition = vec4(position, 1.0); "); src.push("vec4 worldPosition;"); if (quantizedGeometry) { src.push("localPosition = positionsDecodeMatrix * localPosition;"); } if (normals) { if (quantizedGeometry) { src.push("vec4 localNormal = vec4(octDecode(normal.xy), 0.0); "); } else { src.push("vec4 localNormal = vec4(normal, 0.0); "); } src.push("mat4 modelNormalMatrix2 = modelNormalMatrix;"); src.push("mat4 viewNormalMatrix2 = viewNormalMatrix;"); } src.push("mat4 viewMatrix2 = viewMatrix;"); src.push("mat4 modelMatrix2 = modelMatrix;"); if (stationary) { src.push("viewMatrix2[3][0] = viewMatrix2[3][1] = viewMatrix2[3][2] = 0.0;"); } if (billboard === "spherical" || billboard === "cylindrical") { src.push("mat4 modelViewMatrix = viewMatrix2 * modelMatrix2;"); src.push("billboard(modelMatrix2);"); src.push("billboard(viewMatrix2);"); src.push("billboard(modelViewMatrix);"); if (normals) { src.push("mat4 modelViewNormalMatrix = viewNormalMatrix2 * modelNormalMatrix2;"); src.push("billboard(modelNormalMatrix2);"); src.push("billboard(viewNormalMatrix2);"); src.push("billboard(modelViewNormalMatrix);"); } src.push("worldPosition = modelMatrix2 * localPosition;"); src.push("vec4 viewPosition = modelViewMatrix * localPosition;"); } else { src.push("worldPosition = modelMatrix2 * localPosition;"); src.push("worldPosition.xyz = worldPosition.xyz + offset;"); src.push("vec4 viewPosition = viewMatrix2 * worldPosition; "); } if (normals) { src.push("vec3 viewNormal = normalize((viewNormalMatrix2 * modelNormalMatrix2 * localNormal).xyz);"); } src.push("vec3 reflectedColor = vec3(0.0, 0.0, 0.0);"); src.push("vec3 viewLightDir = vec3(0.0, 0.0, -1.0);"); src.push("float lambertian = 1.0;"); if (normals) { for (let i = 0, len = lightsState.lights.length; i < len; i++) { const light = lightsState.lights[i]; if (light.type === "ambient") { continue; } if (light.type === "dir") { if (light.space === "view") { src.push("viewLightDir = normalize(lightDir" + i + ");"); } else { src.push("viewLightDir = normalize((viewMatrix2 * vec4(lightDir" + i + ", 0.0)).xyz);"); } } else if (light.type === "point") { if (light.space === "view") { src.push("viewLightDir = normalize(lightPos" + i + " - viewPosition.xyz);"); } else { src.push("viewLightDir = normalize((viewMatrix2 * vec4(lightPos" + i + ", 0.0)).xyz);"); } } else { continue; } src.push("lambertian = max(dot(-viewNormal, viewLightDir), 0.0);"); src.push("reflectedColor += lambertian * (lightColor" + i + ".rgb * lightColor" + i + ".a);"); } } // TODO: A blending mode for emphasis materials, to select add/multiply/mix //src.push("vColor = vec4((mix(reflectedColor, fillColor.rgb, 0.7)), fillColor.a);"); src.push("vColor = vec4(reflectedColor * fillColor.rgb, fillColor.a);"); //src.push("vColor = vec4(reflectedColor + fillColor.rgb, fillColor.a);"); if (clipping) { src.push("vWorldPosition = worldPosition;"); } if (mesh._geometry._state.primitiveName === "points") { src.push("gl_PointSize = pointSize;"); } src.push("vec4 clipPos = projMatrix * viewPosition;"); if (scene.logarithmicDepthBufferEnabled) { src.push("vFragDepth = 1.0 + clipPos.w;"); src.push("isPerspective = float (isPerspectiveMatrix(projMatrix));"); } src.push("gl_Position = clipPos;"); src.push("}"); return src; } function hasNormals(mesh) { const primitive = mesh._geometry._state.primitiveName; if ((mesh._geometry._state.autoVertexNormals || mesh._geometry._state.normalsBuf) && (primitive === "triangles" || primitive === "triangle-strip" || primitive === "triangle-fan")) { return true; } return false; } function buildFragment$5(mesh) { const scene = mesh.scene; const sectionPlanesState = mesh.scene._sectionPlanesState; const gammaOutput = mesh.scene.gammaOutput; const clipping = sectionPlanesState.getNumAllocatedSectionPlanes() > 0; const src = []; src.push("#version 300 es"); src.push("// Lambertian drawing fragment shader"); src.push("#ifdef GL_FRAGMENT_PRECISION_HIGH"); src.push("precision highp float;"); src.push("precision highp int;"); src.push("#else"); src.push("precision mediump float;"); src.push("precision mediump int;"); src.push("#endif"); if (scene.logarithmicDepthBufferEnabled) { src.push("in float isPerspective;"); src.push("uniform float logDepthBufFC;"); src.push("in float vFragDepth;"); } if (gammaOutput) { src.push("uniform float gammaFactor;"); src.push("vec4 linearToGamma( in vec4 value, in float gammaFactor ) {"); src.push(" return vec4( pow( value.xyz, vec3( 1.0 / gammaFactor ) ), value.w );"); src.push("}"); } if (clipping) { src.push("in vec4 vWorldPosition;"); src.push("uniform bool clippable;"); for (let i = 0, len = sectionPlanesState.getNumAllocatedSectionPlanes(); i < len; i++) { src.push("uniform bool sectionPlaneActive" + i + ";"); src.push("uniform vec3 sectionPlanePos" + i + ";"); src.push("uniform vec3 sectionPlaneDir" + i + ";"); } } src.push("in vec4 vColor;"); src.push("out vec4 outColor;"); src.push("void main(void) {"); if (clipping) { src.push("if (clippable) {"); src.push(" float dist = 0.0;"); for (let i = 0, len = sectionPlanesState.getNumAllocatedSectionPlanes(); i < len; i++) { src.push("if (sectionPlaneActive" + i + ") {"); src.push(" dist += clamp(dot(-sectionPlaneDir" + i + ".xyz, vWorldPosition.xyz - sectionPlanePos" + i + ".xyz), 0.0, 1000.0);"); src.push("}"); } src.push(" if (dist > 0.0) { discard; }"); src.push("}"); } if (mesh._geometry._state.primitiveName === "points") { src.push("vec2 cxy = 2.0 * gl_PointCoord - 1.0;"); src.push("float r = dot(cxy, cxy);"); src.push("if (r > 1.0) {"); src.push(" discard;"); src.push("}"); } if (scene.logarithmicDepthBufferEnabled) { src.push("gl_FragDepth = isPerspective == 0.0 ? gl_FragCoord.z : log2( vFragDepth ) * logDepthBufFC * 0.5;"); } if (gammaOutput) { src.push("outColor = linearToGamma(vColor, gammaFactor);"); } else { src.push("outColor = vColor;"); } src.push("}"); return src; } /** * @author xeolabs / https://github.com/xeolabs */ const ids$1 = new Map$1({}); const tempVec3a$K = math.vec3(); /** * @private */ const EmphasisFillRenderer = function (hash, mesh) { this.id = ids$1.addItem({}); this._hash = hash; this._scene = mesh.scene; this._useCount = 0; this._shaderSource = new EmphasisFillShaderSource(mesh); this._allocate(mesh); }; const xrayFillRenderers = {}; EmphasisFillRenderer.get = function (mesh) { const hash = [ mesh.scene.id, mesh.scene.gammaOutput ? "go" : "", // Gamma input not needed mesh.scene._sectionPlanesState.getHash(), !!mesh._geometry._state.normalsBuf ? "n" : "", mesh._geometry._state.compressGeometry ? "cp" : "", mesh._state.hash ].join(";"); let renderer = xrayFillRenderers[hash]; if (!renderer) { renderer = new EmphasisFillRenderer(hash, mesh); xrayFillRenderers[hash] = renderer; stats.memory.programs++; } renderer._useCount++; return renderer; }; EmphasisFillRenderer.prototype.put = function () { if (--this._useCount === 0) { ids$1.removeItem(this.id); if (this._program) { this._program.destroy(); } delete xrayFillRenderers[this._hash]; stats.memory.programs--; } }; EmphasisFillRenderer.prototype.webglContextRestored = function () { this._program = null; }; EmphasisFillRenderer.prototype.drawMesh = function (frameCtx, mesh, mode) { if (!this._program) { this._allocate(mesh); } const scene = this._scene; const camera = scene.camera; const gl = scene.canvas.gl; const materialState = mode === 0 ? mesh._xrayMaterial._state : (mode === 1 ? mesh._highlightMaterial._state : mesh._selectedMaterial._state); const meshState = mesh._state; const geometryState = mesh._geometry._state; const origin = mesh.origin; if (frameCtx.lastProgramId !== this._program.id) { frameCtx.lastProgramId = this._program.id; this._bindProgram(frameCtx); } gl.uniformMatrix4fv(this._uViewMatrix, false, origin ? frameCtx.getRTCViewMatrix(meshState.originHash, origin) : camera.viewMatrix); gl.uniformMatrix4fv(this._uViewNormalMatrix, false, camera.viewNormalMatrix); if (meshState.clippable) { const numAllocatedSectionPlanes = scene._sectionPlanesState.getNumAllocatedSectionPlanes(); const numSectionPlanes = scene._sectionPlanesState.sectionPlanes.length; if (numAllocatedSectionPlanes > 0) { const sectionPlanes = scene._sectionPlanesState.sectionPlanes; const renderFlags = mesh.renderFlags; for (let sectionPlaneIndex = 0; sectionPlaneIndex < numAllocatedSectionPlanes; sectionPlaneIndex++) { const sectionPlaneUniforms = this._uSectionPlanes[sectionPlaneIndex]; if (sectionPlaneUniforms) { if (sectionPlaneIndex < numSectionPlanes) { const active = renderFlags.sectionPlanesActivePerLayer[sectionPlaneIndex]; gl.uniform1i(sectionPlaneUniforms.active, active ? 1 : 0); if (active) { const sectionPlane = sectionPlanes[sectionPlaneIndex]; if (origin) { const rtcSectionPlanePos = getPlaneRTCPos(sectionPlane.dist, sectionPlane.dir, origin, tempVec3a$K); gl.uniform3fv(sectionPlaneUniforms.pos, rtcSectionPlanePos); } else { gl.uniform3fv(sectionPlaneUniforms.pos, sectionPlane.pos); } gl.uniform3fv(sectionPlaneUniforms.dir, sectionPlane.dir); } } else { gl.uniform1i(sectionPlaneUniforms.active, 0); } } } } } if (materialState.id !== this._lastMaterialId) { const fillColor = materialState.fillColor; const backfaces = materialState.backfaces; if (frameCtx.backfaces !== backfaces) { if (backfaces) { gl.disable(gl.CULL_FACE); } else { gl.enable(gl.CULL_FACE); } frameCtx.backfaces = backfaces; } gl.uniform4f(this._uFillColor, fillColor[0], fillColor[1], fillColor[2], materialState.fillAlpha); this._lastMaterialId = materialState.id; } gl.uniformMatrix4fv(this._uModelMatrix, gl.FALSE, mesh.worldMatrix); if (this._uModelNormalMatrix) { gl.uniformMatrix4fv(this._uModelNormalMatrix, gl.FALSE, mesh.worldNormalMatrix); } if (this._uClippable) { gl.uniform1i(this._uClippable, meshState.clippable); } gl.uniform3fv(this._uOffset, meshState.offset); // Bind VBOs if (geometryState.id !== this._lastGeometryId) { if (this._uPositionsDecodeMatrix) { gl.uniformMatrix4fv(this._uPositionsDecodeMatrix, false, geometryState.positionsDecodeMatrix); } if (this._uUVDecodeMatrix) { gl.uniformMatrix3fv(this._uUVDecodeMatrix, false, geometryState.uvDecodeMatrix); } if (this._aPosition) { this._aPosition.bindArrayBuffer(geometryState.positionsBuf); frameCtx.bindArray++; } if (this._aNormal) { this._aNormal.bindArrayBuffer(geometryState.normalsBuf); frameCtx.bindArray++; } if (geometryState.indicesBuf) { geometryState.indicesBuf.bind(); frameCtx.bindArray++; // gl.drawElements(geometryState.primitive, geometryState.indicesBuf.numItems, geometryState.indicesBuf.itemType, 0); // frameCtx.drawElements++; } else if (geometryState.positionsBuf) ; this._lastGeometryId = geometryState.id; } if (geometryState.indicesBuf) { gl.drawElements(geometryState.primitive, geometryState.indicesBuf.numItems, geometryState.indicesBuf.itemType, 0); frameCtx.drawElements++; } else if (geometryState.positionsBuf) { gl.drawArrays(gl.TRIANGLES, 0, geometryState.positionsBuf.numItems); frameCtx.drawArrays++; } }; EmphasisFillRenderer.prototype._allocate = function (mesh) { const scene = mesh.scene; const lightsState = scene._lightsState; const sectionPlanesState = scene._sectionPlanesState; const gl = scene.canvas.gl; this._program = new Program(gl, this._shaderSource); if (this._program.errors) { this.errors = this._program.errors; return; } const program = this._program; this._uPositionsDecodeMatrix = program.getLocation("positionsDecodeMatrix"); this._uModelMatrix = program.getLocation("modelMatrix"); this._uModelNormalMatrix = program.getLocation("modelNormalMatrix"); this._uViewMatrix = program.getLocation("viewMatrix"); this._uViewNormalMatrix = program.getLocation("viewNormalMatrix"); this._uProjMatrix = program.getLocation("projMatrix"); this._uLightAmbient = []; this._uLightColor = []; this._uLightDir = []; this._uLightPos = []; this._uLightAttenuation = []; for (let i = 0, len = lightsState.lights.length; i < len; i++) { const light = lightsState.lights[i]; switch (light.type) { case "ambient": this._uLightAmbient[i] = program.getLocation("lightAmbient"); break; case "dir": this._uLightColor[i] = program.getLocation("lightColor" + i); this._uLightPos[i] = null; this._uLightDir[i] = program.getLocation("lightDir" + i); break; case "point": this._uLightColor[i] = program.getLocation("lightColor" + i); this._uLightPos[i] = program.getLocation("lightPos" + i); this._uLightDir[i] = null; this._uLightAttenuation[i] = program.getLocation("lightAttenuation" + i); break; } } this._uSectionPlanes = []; for (let i = 0, len = sectionPlanesState.getNumAllocatedSectionPlanes(); i < len; i++) { this._uSectionPlanes.push({ active: program.getLocation("sectionPlaneActive" + i), pos: program.getLocation("sectionPlanePos" + i), dir: program.getLocation("sectionPlaneDir" + i) }); } this._uFillColor = program.getLocation("fillColor"); this._aPosition = program.getAttribute("position"); this._aNormal = program.getAttribute("normal"); this._uClippable = program.getLocation("clippable"); this._uGammaFactor = program.getLocation("gammaFactor"); this._uOffset = program.getLocation("offset"); if (scene.logarithmicDepthBufferEnabled ) { this._uLogDepthBufFC = program.getLocation("logDepthBufFC"); } this._lastMaterialId = null; this._lastVertexBufsId = null; this._lastGeometryId = null; }; EmphasisFillRenderer.prototype._bindProgram = function (frameCtx) { const scene = this._scene; const gl = scene.canvas.gl; const lightsState = scene._lightsState; const camera = scene.camera; const project = camera.project; const program = this._program; program.bind(); frameCtx.useProgram++; frameCtx.textureUnit = 0; this._lastMaterialId = null; this._lastVertexBufsId = null; this._lastGeometryId = null; this._lastIndicesBufId = null; gl.uniformMatrix4fv(this._uViewNormalMatrix, false, camera.normalMatrix); gl.uniformMatrix4fv(this._uProjMatrix, false, project.matrix); if (scene.logarithmicDepthBufferEnabled ) { const logDepthBufFC = 2.0 / (Math.log(project.far + 1.0) / Math.LN2); gl.uniform1f(this._uLogDepthBufFC, logDepthBufFC); } for (let i = 0, len = lightsState.lights.length; i < len; i++) { const light = lightsState.lights[i]; if (this._uLightAmbient[i]) { gl.uniform4f(this._uLightAmbient[i], light.color[0], light.color[1], light.color[2], light.intensity); } else { if (this._uLightColor[i]) { gl.uniform4f(this._uLightColor[i], light.color[0], light.color[1], light.color[2], light.intensity); } if (this._uLightPos[i]) { gl.uniform3fv(this._uLightPos[i], light.pos); if (this._uLightAttenuation[i]) { gl.uniform1f(this._uLightAttenuation[i], light.attenuation); } } if (this._uLightDir[i]) { gl.uniform3fv(this._uLightDir[i], light.dir); } } } if (this._uGammaFactor) { gl.uniform1f(this._uGammaFactor, scene.gammaFactor); } }; /** * @private */ class EmphasisEdgesShaderSource { constructor(mesh) { this.vertex = buildVertex$4(mesh); this.fragment = buildFragment$4(mesh); } } function buildVertex$4(mesh) { const scene = mesh.scene; const sectionPlanesState = scene._sectionPlanesState; const clipping = sectionPlanesState.getNumAllocatedSectionPlanes() > 0; const quantizedGeometry = !!mesh._geometry._state.compressGeometry; const billboard = mesh._state.billboard; const stationary = mesh._state.stationary; const src = []; src.push("#version 300 es"); src.push("// Edges drawing vertex shader"); src.push("in vec3 position;"); src.push("uniform mat4 modelMatrix;"); src.push("uniform mat4 viewMatrix;"); src.push("uniform mat4 projMatrix;"); src.push("uniform vec4 edgeColor;"); src.push("uniform vec3 offset;"); if (quantizedGeometry) { src.push("uniform mat4 positionsDecodeMatrix;"); } if (scene.logarithmicDepthBufferEnabled) { src.push("uniform float logDepthBufFC;"); src.push("out float vFragDepth;"); src.push("bool isPerspectiveMatrix(mat4 m) {"); src.push(" return (m[2][3] == - 1.0);"); src.push("}"); src.push("out float isPerspective;"); } if (clipping) { src.push("out vec4 vWorldPosition;"); } src.push("out vec4 vColor;"); if (billboard === "spherical" || billboard === "cylindrical") { src.push("void billboard(inout mat4 mat) {"); src.push(" mat[0][0] = 1.0;"); src.push(" mat[0][1] = 0.0;"); src.push(" mat[0][2] = 0.0;"); if (billboard === "spherical") { src.push(" mat[1][0] = 0.0;"); src.push(" mat[1][1] = 1.0;"); src.push(" mat[1][2] = 0.0;"); } src.push(" mat[2][0] = 0.0;"); src.push(" mat[2][1] = 0.0;"); src.push(" mat[2][2] =1.0;"); src.push("}"); } src.push("void main(void) {"); src.push("vec4 localPosition = vec4(position, 1.0); "); src.push("vec4 worldPosition;"); if (quantizedGeometry) { src.push("localPosition = positionsDecodeMatrix * localPosition;"); } src.push("mat4 viewMatrix2 = viewMatrix;"); src.push("mat4 modelMatrix2 = modelMatrix;"); if (stationary) { src.push("viewMatrix2[3][0] = viewMatrix2[3][1] = viewMatrix2[3][2] = 0.0;"); } if (billboard === "spherical" || billboard === "cylindrical") { src.push("mat4 modelViewMatrix = viewMatrix2 * modelMatrix2;"); src.push("billboard(modelMatrix2);"); src.push("billboard(viewMatrix2);"); src.push("billboard(modelViewMatrix);"); src.push("worldPosition = modelMatrix2 * localPosition;"); src.push("worldPosition.xyz = worldPosition.xyz + offset;"); src.push("vec4 viewPosition = modelViewMatrix * localPosition;"); } else { src.push("worldPosition = modelMatrix2 * localPosition;"); src.push("worldPosition.xyz = worldPosition.xyz + offset;"); src.push("vec4 viewPosition = viewMatrix2 * worldPosition; "); } src.push("vColor = edgeColor;"); if (clipping) { src.push("vWorldPosition = worldPosition;"); } src.push("vec4 clipPos = projMatrix * viewPosition;"); if (scene.logarithmicDepthBufferEnabled) { src.push("vFragDepth = 1.0 + clipPos.w;"); src.push("isPerspective = float (isPerspectiveMatrix(projMatrix));"); } src.push("gl_Position = clipPos;"); src.push("}"); return src; } function buildFragment$4(mesh) { const scene = mesh.scene; const sectionPlanesState = mesh.scene._sectionPlanesState; const gammaOutput = mesh.scene.gammaOutput; const clipping = sectionPlanesState.getNumAllocatedSectionPlanes() > 0; const src = []; src.push("#version 300 es"); src.push("// Edges drawing fragment shader"); src.push("#ifdef GL_FRAGMENT_PRECISION_HIGH"); src.push("precision highp float;"); src.push("precision highp int;"); src.push("#else"); src.push("precision mediump float;"); src.push("precision mediump int;"); src.push("#endif"); if (scene.logarithmicDepthBufferEnabled) { src.push("in float isPerspective;"); src.push("uniform float logDepthBufFC;"); src.push("in float vFragDepth;"); } if (gammaOutput) { src.push("uniform float gammaFactor;"); src.push("vec4 linearToGamma( in vec4 value, in float gammaFactor ) {"); src.push(" return vec4( pow( value.xyz, vec3( 1.0 / gammaFactor ) ), value.w );"); src.push("}"); } if (clipping) { src.push("in vec4 vWorldPosition;"); src.push("uniform bool clippable;"); for (let i = 0, len = sectionPlanesState.getNumAllocatedSectionPlanes(); i < len; i++) { src.push("uniform bool sectionPlaneActive" + i + ";"); src.push("uniform vec3 sectionPlanePos" + i + ";"); src.push("uniform vec3 sectionPlaneDir" + i + ";"); } } src.push("in vec4 vColor;"); src.push("out vec4 outColor;"); src.push("void main(void) {"); if (clipping) { src.push("if (clippable) {"); src.push(" float dist = 0.0;"); for (let i = 0, len = sectionPlanesState.getNumAllocatedSectionPlanes(); i < len; i++) { src.push("if (sectionPlaneActive" + i + ") {"); src.push(" dist += clamp(dot(-sectionPlaneDir" + i + ".xyz, vWorldPosition.xyz - sectionPlanePos" + i + ".xyz), 0.0, 1000.0);"); src.push("}"); } src.push(" if (dist > 0.0) { discard; }"); src.push("}"); } if (scene.logarithmicDepthBufferEnabled) { src.push("gl_FragDepth = isPerspective == 0.0 ? gl_FragCoord.z : log2( vFragDepth ) * logDepthBufFC * 0.5;"); } if (gammaOutput) { src.push("outColor = linearToGamma(vColor, gammaFactor);"); } else { src.push("outColor = vColor;"); } src.push("}"); return src; } /** * @author xeolabs / https://github.com/xeolabs */ const ids = new Map$1({}); const tempVec3a$J = math.vec3(); /** * @private */ const EmphasisEdgesRenderer = function (hash, mesh) { this.id = ids.addItem({}); this._hash = hash; this._scene = mesh.scene; this._useCount = 0; this._shaderSource = new EmphasisEdgesShaderSource(mesh); this._allocate(mesh); }; const renderers$4 = {}; EmphasisEdgesRenderer.get = function (mesh) { const hash = [ mesh.scene.id, mesh.scene.gammaOutput ? "go" : "", // Gamma input not needed mesh.scene._sectionPlanesState.getHash(), mesh._geometry._state.compressGeometry ? "cp" : "", mesh._state.hash ].join(";"); let renderer = renderers$4[hash]; if (!renderer) { renderer = new EmphasisEdgesRenderer(hash, mesh); renderers$4[hash] = renderer; stats.memory.programs++; } renderer._useCount++; return renderer; }; EmphasisEdgesRenderer.prototype.put = function () { if (--this._useCount === 0) { ids.removeItem(this.id); if (this._program) { this._program.destroy(); } delete renderers$4[this._hash]; stats.memory.programs--; } }; EmphasisEdgesRenderer.prototype.webglContextRestored = function () { this._program = null; }; EmphasisEdgesRenderer.prototype.drawMesh = function (frameCtx, mesh, mode) { if (!this._program) { this._allocate(mesh); } const scene = this._scene; const camera = scene.camera; const gl = scene.canvas.gl; let materialState; const meshState = mesh._state; const geometry = mesh._geometry; const geometryState = geometry._state; const origin = mesh.origin; if (frameCtx.lastProgramId !== this._program.id) { frameCtx.lastProgramId = this._program.id; this._bindProgram(frameCtx); } gl.uniformMatrix4fv(this._uViewMatrix, false, origin ? frameCtx.getRTCViewMatrix(meshState.originHash, origin) : camera.viewMatrix); if (meshState.clippable) { const numAllocatedSectionPlanes = scene._sectionPlanesState.getNumAllocatedSectionPlanes(); const numSectionPlanes = scene._sectionPlanesState.sectionPlanes.length; if (numAllocatedSectionPlanes > 0) { const sectionPlanes = scene._sectionPlanesState.sectionPlanes; const renderFlags = mesh.renderFlags; for (let sectionPlaneIndex = 0; sectionPlaneIndex < numAllocatedSectionPlanes; sectionPlaneIndex++) { const sectionPlaneUniforms = this._uSectionPlanes[sectionPlaneIndex]; if (sectionPlaneUniforms) { if (sectionPlaneIndex < numSectionPlanes) { const active = renderFlags.sectionPlanesActivePerLayer[sectionPlaneIndex]; gl.uniform1i(sectionPlaneUniforms.active, active ? 1 : 0); if (active) { const sectionPlane = sectionPlanes[sectionPlaneIndex]; if (origin) { const rtcSectionPlanePos = getPlaneRTCPos(sectionPlane.dist, sectionPlane.dir, origin, tempVec3a$J); gl.uniform3fv(sectionPlaneUniforms.pos, rtcSectionPlanePos); } else { gl.uniform3fv(sectionPlaneUniforms.pos, sectionPlane.pos); } gl.uniform3fv(sectionPlaneUniforms.dir, sectionPlane.dir); } } else { gl.uniform1i(sectionPlaneUniforms.active, 0); } } } } } switch (mode) { case 0: materialState = mesh._xrayMaterial._state; break; case 1: materialState = mesh._highlightMaterial._state; break; case 2: materialState = mesh._selectedMaterial._state; break; case 3: default: materialState = mesh._edgeMaterial._state; break; } if (materialState.id !== this._lastMaterialId) { const backfaces = materialState.backfaces; if (frameCtx.backfaces !== backfaces) { if (backfaces) { gl.disable(gl.CULL_FACE); } else { gl.enable(gl.CULL_FACE); } frameCtx.backfaces = backfaces; } if (frameCtx.lineWidth !== materialState.edgeWidth) { gl.lineWidth(materialState.edgeWidth); frameCtx.lineWidth = materialState.edgeWidth; } if (this._uEdgeColor) { const edgeColor = materialState.edgeColor; const edgeAlpha = materialState.edgeAlpha; gl.uniform4f(this._uEdgeColor, edgeColor[0], edgeColor[1], edgeColor[2], edgeAlpha); } this._lastMaterialId = materialState.id; } gl.uniformMatrix4fv(this._uModelMatrix, gl.FALSE, mesh.worldMatrix); if (this._uClippable) { gl.uniform1i(this._uClippable, meshState.clippable); } gl.uniform3fv(this._uOffset, meshState.offset); // Bind VBOs let indicesBuf; if (geometryState.primitive === gl.TRIANGLES) { indicesBuf = geometry._getEdgeIndices(); } else if (geometryState.primitive === gl.LINES) { indicesBuf = geometryState.indicesBuf; } if (indicesBuf) { if (geometryState.id !== this._lastGeometryId) { if (this._uPositionsDecodeMatrix) { gl.uniformMatrix4fv(this._uPositionsDecodeMatrix, false, geometryState.positionsDecodeMatrix); } if (this._aPosition) { this._aPosition.bindArrayBuffer(geometryState.positionsBuf, geometryState.compressGeometry ? gl.UNSIGNED_SHORT : gl.FLOAT); frameCtx.bindArray++; } indicesBuf.bind(); frameCtx.bindArray++; this._lastGeometryId = geometryState.id; } gl.drawElements(gl.LINES, indicesBuf.numItems, indicesBuf.itemType, 0); frameCtx.drawElements++; } }; EmphasisEdgesRenderer.prototype._allocate = function (mesh) { const scene = mesh.scene; const gl = scene.canvas.gl; const sectionPlanesState = scene._sectionPlanesState; this._program = new Program(gl, this._shaderSource); if (this._program.errors) { this.errors = this._program.errors; return; } const program = this._program; this._uPositionsDecodeMatrix = program.getLocation("positionsDecodeMatrix"); this._uModelMatrix = program.getLocation("modelMatrix"); this._uViewMatrix = program.getLocation("viewMatrix"); this._uProjMatrix = program.getLocation("projMatrix"); this._uSectionPlanes = []; for (let i = 0, len = sectionPlanesState.getNumAllocatedSectionPlanes(); i < len; i++) { this._uSectionPlanes.push({ active: program.getLocation("sectionPlaneActive" + i), pos: program.getLocation("sectionPlanePos" + i), dir: program.getLocation("sectionPlaneDir" + i) }); } this._uEdgeColor = program.getLocation("edgeColor"); this._aPosition = program.getAttribute("position"); this._uClippable = program.getLocation("clippable"); this._uGammaFactor = program.getLocation("gammaFactor"); this._uOffset = program.getLocation("offset"); if (scene.logarithmicDepthBufferEnabled ) { this._uLogDepthBufFC = program.getLocation("logDepthBufFC"); } this._lastMaterialId = null; this._lastVertexBufsId = null; this._lastGeometryId = null; }; EmphasisEdgesRenderer.prototype._bindProgram = function (frameCtx) { const program = this._program; const scene = this._scene; const gl = scene.canvas.gl; const camera = scene.camera; const project = camera.project; program.bind(); frameCtx.useProgram++; this._lastMaterialId = null; this._lastVertexBufsId = null; this._lastGeometryId = null; gl.uniformMatrix4fv(this._uProjMatrix, false, project.matrix); if (scene.logarithmicDepthBufferEnabled ) { const logDepthBufFC = 2.0 / (Math.log(project.far + 1.0) / Math.LN2); gl.uniform1f(this._uLogDepthBufFC, logDepthBufFC); } if (this._uGammaFactor) { gl.uniform1f(this._uGammaFactor, scene.gammaFactor); } }; /** * @author xeolabs / https://github.com/xeolabs */ /** * @private */ class PickMeshShaderSource { constructor(mesh) { this.vertex = buildVertex$3(mesh); this.fragment = buildFragment$3(mesh); } } function buildVertex$3(mesh) { const scene = mesh.scene; const sectionPlanesState = scene._sectionPlanesState; const clipping = sectionPlanesState.getNumAllocatedSectionPlanes() > 0; const quantizedGeometry = !!mesh._geometry._state.compressGeometry; const billboard = mesh._state.billboard; const stationary = mesh._state.stationary; const src = []; src.push('#version 300 es'); src.push("// Mesh picking vertex shader"); src.push("in vec3 position;"); src.push("uniform mat4 modelMatrix;"); src.push("uniform mat4 viewMatrix;"); src.push("uniform mat4 projMatrix;"); src.push("out vec4 vViewPosition;"); src.push("uniform vec3 offset;"); if (quantizedGeometry) { src.push("uniform mat4 positionsDecodeMatrix;"); } if (clipping) { src.push("out vec4 vWorldPosition;"); } if (scene.logarithmicDepthBufferEnabled) { src.push("uniform float logDepthBufFC;"); src.push("out float vFragDepth;"); src.push("bool isPerspectiveMatrix(mat4 m) {"); src.push(" return (m[2][3] == - 1.0);"); src.push("}"); src.push("out float isPerspective;"); } if (billboard === "spherical" || billboard === "cylindrical") { src.push("void billboard(inout mat4 mat) {"); src.push(" mat[0][0] = 1.0;"); src.push(" mat[0][1] = 0.0;"); src.push(" mat[0][2] = 0.0;"); if (billboard === "spherical") { src.push(" mat[1][0] = 0.0;"); src.push(" mat[1][1] = 1.0;"); src.push(" mat[1][2] = 0.0;"); } src.push(" mat[2][0] = 0.0;"); src.push(" mat[2][1] = 0.0;"); src.push(" mat[2][2] =1.0;"); src.push("}"); } src.push("uniform vec2 pickClipPos;"); src.push("vec4 remapClipPos(vec4 clipPos) {"); src.push(" clipPos.xy /= clipPos.w;"); src.push(" clipPos.xy -= pickClipPos;"); src.push(" clipPos.xy *= clipPos.w;"); src.push(" return clipPos;"); src.push("}"); src.push("void main(void) {"); src.push("vec4 localPosition = vec4(position, 1.0); "); if (quantizedGeometry) { src.push("localPosition = positionsDecodeMatrix * localPosition;"); } src.push("mat4 viewMatrix2 = viewMatrix;"); src.push("mat4 modelMatrix2 = modelMatrix;"); if (stationary) { src.push("viewMatrix2[3][0] = viewMatrix2[3][1] = viewMatrix2[3][2] = 0.0;"); } if (billboard === "spherical" || billboard === "cylindrical") { src.push("mat4 modelViewMatrix = viewMatrix2 * modelMatrix2;"); src.push("billboard(modelMatrix2);"); src.push("billboard(viewMatrix2);"); } src.push(" vec4 worldPosition = modelMatrix2 * localPosition;"); src.push(" worldPosition.xyz = worldPosition.xyz + offset;"); src.push(" vec4 viewPosition = viewMatrix2 * worldPosition;"); if (clipping) { src.push(" vWorldPosition = worldPosition;"); } src.push("vec4 clipPos = projMatrix * viewPosition;"); if (scene.logarithmicDepthBufferEnabled) { src.push("vFragDepth = 1.0 + clipPos.w;"); src.push("isPerspective = float (isPerspectiveMatrix(projMatrix));"); } src.push("gl_Position = remapClipPos(clipPos);"); src.push("}"); return src; } function buildFragment$3(mesh) { const scene = mesh.scene; const sectionPlanesState = scene._sectionPlanesState; const clipping = sectionPlanesState.getNumAllocatedSectionPlanes() > 0; const src = []; src.push('#version 300 es'); src.push("// Mesh picking fragment shader"); src.push("#ifdef GL_FRAGMENT_PRECISION_HIGH"); src.push("precision highp float;"); src.push("precision highp int;"); src.push("#else"); src.push("precision mediump float;"); src.push("precision mediump int;"); src.push("#endif"); if (scene.logarithmicDepthBufferEnabled) { src.push("in float isPerspective;"); src.push("uniform float logDepthBufFC;"); src.push("in float vFragDepth;"); } src.push("uniform vec4 pickColor;"); if (clipping) { src.push("uniform bool clippable;"); src.push("in vec4 vWorldPosition;"); for (var i = 0; i < sectionPlanesState.getNumAllocatedSectionPlanes(); i++) { src.push("uniform bool sectionPlaneActive" + i + ";"); src.push("uniform vec3 sectionPlanePos" + i + ";"); src.push("uniform vec3 sectionPlaneDir" + i + ";"); } } src.push("out vec4 outColor;"); src.push("void main(void) {"); if (clipping) { src.push("if (clippable) {"); src.push(" float dist = 0.0;"); for (var i = 0; i < sectionPlanesState.getNumAllocatedSectionPlanes(); i++) { src.push("if (sectionPlaneActive" + i + ") {"); src.push(" dist += clamp(dot(-sectionPlaneDir" + i + ".xyz, vWorldPosition.xyz - sectionPlanePos" + i + ".xyz), 0.0, 1000.0);"); src.push("}"); } src.push(" if (dist > 0.0) { discard; }"); src.push("}"); } if (scene.logarithmicDepthBufferEnabled) { src.push("gl_FragDepth = isPerspective == 0.0 ? gl_FragCoord.z : log2( vFragDepth ) * logDepthBufFC * 0.5;"); } src.push(" outColor = pickColor; "); src.push("}"); return src; } /** * @author xeolabs / https://github.com/xeolabs */ const tempVec3a$I = math.vec3(); // No ID, because there is exactly one PickMeshRenderer per scene /** * @private */ const PickMeshRenderer = function (hash, mesh) { this._hash = hash; this._shaderSource = new PickMeshShaderSource(mesh); this._scene = mesh.scene; this._useCount = 0; this._allocate(mesh); }; const renderers$3 = {}; PickMeshRenderer.get = function (mesh) { const hash = [ mesh.scene.canvas.canvas.id, mesh.scene._sectionPlanesState.getHash(), mesh._geometry._state.hash, mesh._state.hash ].join(";"); let renderer = renderers$3[hash]; if (!renderer) { renderer = new PickMeshRenderer(hash, mesh); if (renderer.errors) { console.log(renderer.errors.join("\n")); return null; } renderers$3[hash] = renderer; stats.memory.programs++; } renderer._useCount++; return renderer; }; PickMeshRenderer.prototype.put = function () { if (--this._useCount === 0) { if (this._program) { this._program.destroy(); } delete renderers$3[this._hash]; stats.memory.programs--; } }; PickMeshRenderer.prototype.webglContextRestored = function () { this._program = null; }; PickMeshRenderer.prototype.drawMesh = function (frameCtx, mesh) { if (!this._program) { this._allocate(mesh); } const scene = this._scene; const gl = scene.canvas.gl; const meshState = mesh._state; const materialState = mesh._material._state; const geometryState = mesh._geometry._state; const origin = mesh.origin; if (frameCtx.lastProgramId !== this._program.id) { frameCtx.lastProgramId = this._program.id; this._bindProgram(frameCtx); } gl.uniformMatrix4fv(this._uViewMatrix, false, origin ? frameCtx.getRTCPickViewMatrix(meshState.originHash, origin) : frameCtx.pickViewMatrix); if (meshState.clippable) { const numAllocatedSectionPlanes = scene._sectionPlanesState.getNumAllocatedSectionPlanes(); const numSectionPlanes = scene._sectionPlanesState.sectionPlanes.length; if (numAllocatedSectionPlanes > 0) { const sectionPlanes = scene._sectionPlanesState.sectionPlanes; const renderFlags = mesh.renderFlags; for (let sectionPlaneIndex = 0; sectionPlaneIndex < numAllocatedSectionPlanes; sectionPlaneIndex++) { const sectionPlaneUniforms = this._uSectionPlanes[sectionPlaneIndex]; if (sectionPlaneUniforms) { if (sectionPlaneIndex < numSectionPlanes) { const active = renderFlags.sectionPlanesActivePerLayer[sectionPlaneIndex]; gl.uniform1i(sectionPlaneUniforms.active, active ? 1 : 0); if (active) { const sectionPlane = sectionPlanes[sectionPlaneIndex]; if (origin) { const rtcSectionPlanePos = getPlaneRTCPos(sectionPlane.dist, sectionPlane.dir, origin, tempVec3a$I); gl.uniform3fv(sectionPlaneUniforms.pos, rtcSectionPlanePos); } else { gl.uniform3fv(sectionPlaneUniforms.pos, sectionPlane.pos); } gl.uniform3fv(sectionPlaneUniforms.dir, sectionPlane.dir); } } else { gl.uniform1i(sectionPlaneUniforms.active, 0); } } } } } if (materialState.id !== this._lastMaterialId) { const backfaces = materialState.backfaces; if (frameCtx.backfaces !== backfaces) { if (backfaces) { gl.disable(gl.CULL_FACE); } else { gl.enable(gl.CULL_FACE); } frameCtx.backfaces = backfaces; } const frontface = materialState.frontface; if (frameCtx.frontface !== frontface) { if (frontface) { gl.frontFace(gl.CCW); } else { gl.frontFace(gl.CW); } frameCtx.frontface = frontface; } this._lastMaterialId = materialState.id; } gl.uniformMatrix4fv(this._uProjMatrix, false, frameCtx.pickProjMatrix); gl.uniformMatrix4fv(this._uModelMatrix, false, mesh.worldMatrix); if (this._uClippable) { gl.uniform1i(this._uClippable, mesh._state.clippable); } gl.uniform3fv(this._uOffset, mesh._state.offset); if (geometryState.id !== this._lastGeometryId) { if (this._uPositionsDecodeMatrix) { gl.uniformMatrix4fv(this._uPositionsDecodeMatrix, false, geometryState.positionsDecodeMatrix); } if (this._aPosition) { this._aPosition.bindArrayBuffer(geometryState.positionsBuf, geometryState.compressGeometry ? gl.UNSIGNED_SHORT : gl.FLOAT); frameCtx.bindArray++; } if (geometryState.indicesBuf) { geometryState.indicesBuf.bind(); frameCtx.bindArray++; } this._lastGeometryId = geometryState.id; } // Mesh-indexed color var pickID = mesh._state.pickID; const a = pickID >> 24 & 0xFF; const b = pickID >> 16 & 0xFF; const g = pickID >> 8 & 0xFF; const r = pickID & 0xFF; gl.uniform4f(this._uPickColor, r / 255, g / 255, b / 255, a / 255); gl.uniform2fv(this._uPickClipPos, frameCtx.pickClipPos); if (geometryState.indicesBuf) { gl.drawElements(geometryState.primitive, geometryState.indicesBuf.numItems, geometryState.indicesBuf.itemType, 0); frameCtx.drawElements++; } else if (geometryState.positions) { gl.drawArrays(gl.TRIANGLES, 0, geometryState.positions.numItems); } }; PickMeshRenderer.prototype._allocate = function (mesh) { const scene = mesh.scene; const gl = scene.canvas.gl; this._program = new Program(gl, this._shaderSource); if (this._program.errors) { this.errors = this._program.errors; return; } const program = this._program; this._uPositionsDecodeMatrix = program.getLocation("positionsDecodeMatrix"); this._uModelMatrix = program.getLocation("modelMatrix"); this._uViewMatrix = program.getLocation("viewMatrix"); this._uProjMatrix = program.getLocation("projMatrix"); this._uSectionPlanes = []; const clips = scene._sectionPlanesState.sectionPlanes; for (let i = 0, len = clips.length; i < len; i++) { this._uSectionPlanes.push({ active: program.getLocation("sectionPlaneActive" + i), pos: program.getLocation("sectionPlanePos" + i), dir: program.getLocation("sectionPlaneDir" + i) }); } this._aPosition = program.getAttribute("position"); this._uClippable = program.getLocation("clippable"); this._uPickColor = program.getLocation("pickColor"); this._uPickClipPos = program.getLocation("pickClipPos"); this._uOffset = program.getLocation("offset"); if (scene.logarithmicDepthBufferEnabled ) { this._uLogDepthBufFC = program.getLocation("logDepthBufFC"); } this._lastMaterialId = null; this._lastGeometryId = null; }; PickMeshRenderer.prototype._bindProgram = function (frameCtx) { const scene = this._scene; const gl = scene.canvas.gl; const project = scene.camera.project; this._program.bind(); frameCtx.useProgram++; if (scene.logarithmicDepthBufferEnabled ) { const logDepthBufFC = 2.0 / (Math.log(project.far + 1.0) / Math.LN2); gl.uniform1f(this._uLogDepthBufFC, logDepthBufFC); } this._lastMaterialId = null; this._lastGeometryId = null; }; /** * @private */ class PickTriangleShaderSource { constructor(mesh) { this.vertex = buildVertex$2(mesh); this.fragment = buildFragment$2(mesh); } } function buildVertex$2(mesh) { const scene = mesh.scene; const sectionPlanesState = scene._sectionPlanesState; const clipping = sectionPlanesState.getNumAllocatedSectionPlanes() > 0; const quantizedGeometry = !!mesh._geometry._state.compressGeometry; const src = []; src.push('#version 300 es'); src.push("// Surface picking vertex shader"); src.push("in vec3 position;"); src.push("in vec4 color;"); src.push("uniform mat4 modelMatrix;"); src.push("uniform mat4 viewMatrix;"); src.push("uniform mat4 projMatrix;"); src.push("uniform vec3 offset;"); if (clipping) { src.push("uniform bool clippable;"); src.push("out vec4 vWorldPosition;"); } if (scene.logarithmicDepthBufferEnabled) { src.push("uniform float logDepthBufFC;"); src.push("out float vFragDepth;"); src.push("bool isPerspectiveMatrix(mat4 m) {"); src.push(" return (m[2][3] == - 1.0);"); src.push("}"); src.push("out float isPerspective;"); } src.push("uniform vec2 pickClipPos;"); src.push("vec4 remapClipPos(vec4 clipPos) {"); src.push(" clipPos.xy /= clipPos.w;"); src.push(" clipPos.xy -= pickClipPos;"); src.push(" clipPos.xy *= clipPos.w;"); src.push(" return clipPos;"); src.push("}"); src.push("out vec4 vColor;"); if (quantizedGeometry) { src.push("uniform mat4 positionsDecodeMatrix;"); } src.push("void main(void) {"); src.push("vec4 localPosition = vec4(position, 1.0); "); if (quantizedGeometry) { src.push("localPosition = positionsDecodeMatrix * localPosition;"); } src.push(" vec4 worldPosition = modelMatrix * localPosition; "); src.push(" worldPosition.xyz = worldPosition.xyz + offset;"); src.push(" vec4 viewPosition = viewMatrix * worldPosition;"); if (clipping) { src.push(" vWorldPosition = worldPosition;"); } src.push(" vColor = color;"); src.push("vec4 clipPos = projMatrix * viewPosition;"); if (scene.logarithmicDepthBufferEnabled) { src.push("vFragDepth = 1.0 + clipPos.w;"); src.push("isPerspective = float (isPerspectiveMatrix(projMatrix));"); } src.push("gl_Position = remapClipPos(clipPos);"); src.push("}"); return src; } function buildFragment$2(mesh) { const scene = mesh.scene; const sectionPlanesState = scene._sectionPlanesState; const clipping = sectionPlanesState.getNumAllocatedSectionPlanes() > 0; const src = []; src.push('#version 300 es'); src.push("// Surface picking fragment shader"); src.push("#ifdef GL_FRAGMENT_PRECISION_HIGH"); src.push("precision highp float;"); src.push("precision highp int;"); src.push("#else"); src.push("precision mediump float;"); src.push("precision mediump int;"); src.push("#endif"); src.push("in vec4 vColor;"); if (scene.logarithmicDepthBufferEnabled) { src.push("in float isPerspective;"); src.push("uniform float logDepthBufFC;"); src.push("in float vFragDepth;"); } if (clipping) { src.push("uniform bool clippable;"); src.push("in vec4 vWorldPosition;"); for (let i = 0; i < sectionPlanesState.getNumAllocatedSectionPlanes(); i++) { src.push("uniform bool sectionPlaneActive" + i + ";"); src.push("uniform vec3 sectionPlanePos" + i + ";"); src.push("uniform vec3 sectionPlaneDir" + i + ";"); } } src.push("out vec4 outColor;"); src.push("void main(void) {"); if (clipping) { src.push("if (clippable) {"); src.push(" float dist = 0.0;"); for (let i = 0; i < sectionPlanesState.getNumAllocatedSectionPlanes(); i++) { src.push("if (sectionPlaneActive" + i + ") {"); src.push(" dist += clamp(dot(-sectionPlaneDir" + i + ".xyz, vWorldPosition.xyz - sectionPlanePos" + i + ".xyz), 0.0, 1000.0);"); src.push("}"); } src.push(" if (dist > 0.0) { discard; }"); src.push("}"); } if (scene.logarithmicDepthBufferEnabled) { src.push("gl_FragDepth = isPerspective == 0.0 ? gl_FragCoord.z : log2( vFragDepth ) * logDepthBufFC * 0.5;"); } src.push(" outColor = vColor;"); src.push("}"); return src; } /** * @author xeolabs / https://github.com/xeolabs */ const tempVec3a$H = math.vec3(); /** * @private */ const PickTriangleRenderer = function (hash, mesh) { this._hash = hash; this._scene = mesh.scene; this._useCount = 0; this._shaderSource = new PickTriangleShaderSource(mesh); this._allocate(mesh); }; const renderers$2 = {}; PickTriangleRenderer.get = function (mesh) { const hash = [ mesh.scene.canvas.canvas.id, mesh.scene._sectionPlanesState.getHash(), mesh._geometry._state.compressGeometry ? "cp" : "", mesh._state.hash ].join(";"); let renderer = renderers$2[hash]; if (!renderer) { renderer = new PickTriangleRenderer(hash, mesh); if (renderer.errors) { console.log(renderer.errors.join("\n")); return null; } renderers$2[hash] = renderer; stats.memory.programs++; } renderer._useCount++; return renderer; }; PickTriangleRenderer.prototype.put = function () { if (--this._useCount === 0) { if (this._program) { this._program.destroy(); } delete renderers$2[this._hash]; stats.memory.programs--; } }; PickTriangleRenderer.prototype.webglContextRestored = function () { this._program = null; }; PickTriangleRenderer.prototype.drawMesh = function (frameCtx, mesh) { if (!this._program) { this._allocate(mesh); } const scene = this._scene; const gl = scene.canvas.gl; const meshState = mesh._state; const materialState = mesh._material._state; const geometry = mesh._geometry; const geometryState = mesh._geometry._state; const origin = mesh.origin; const backfaces = materialState.backfaces; const frontface = materialState.frontface; const project = scene.camera.project; const positionsBuf = geometry._getPickTrianglePositions(); const pickColorsBuf = geometry._getPickTriangleColors(); this._program.bind(); frameCtx.useProgram++; if (scene.logarithmicDepthBufferEnabled ) { const logDepthBufFC = 2.0 / (Math.log(project.far + 1.0) / Math.LN2); gl.uniform1f(this._uLogDepthBufFC, logDepthBufFC); } gl.uniformMatrix4fv(this._uViewMatrix, false, origin ? frameCtx.getRTCPickViewMatrix(meshState.originHash, origin) : frameCtx.pickViewMatrix); if (meshState.clippable) { const numAllocatedSectionPlanes = scene._sectionPlanesState.getNumAllocatedSectionPlanes(); const numSectionPlanes = scene._sectionPlanesState.sectionPlanes.length; if (numAllocatedSectionPlanes > 0) { const sectionPlanes = scene._sectionPlanesState.sectionPlanes; const renderFlags = mesh.renderFlags; for (let sectionPlaneIndex = 0; sectionPlaneIndex < numAllocatedSectionPlanes; sectionPlaneIndex++) { const sectionPlaneUniforms = this._uSectionPlanes[sectionPlaneIndex]; if (sectionPlaneUniforms) { if (sectionPlaneIndex < numSectionPlanes) { const active = renderFlags.sectionPlanesActivePerLayer[sectionPlaneIndex]; gl.uniform1i(sectionPlaneUniforms.active, active ? 1 : 0); if (active) { const sectionPlane = sectionPlanes[sectionPlaneIndex]; if (origin) { const rtcSectionPlanePos = getPlaneRTCPos(sectionPlane.dist, sectionPlane.dir, origin, tempVec3a$H); gl.uniform3fv(sectionPlaneUniforms.pos, rtcSectionPlanePos); } else { gl.uniform3fv(sectionPlaneUniforms.pos, sectionPlane.pos); } gl.uniform3fv(sectionPlaneUniforms.dir, sectionPlane.dir); } } else { gl.uniform1i(sectionPlaneUniforms.active, 0); } } } } } gl.uniformMatrix4fv(this._uProjMatrix, false, frameCtx.pickProjMatrix); if (scene.logarithmicDepthBufferEnabled) { gl.uniform1f(this._uZFar, scene.camera.project.far); } if (frameCtx.backfaces !== backfaces) { if (backfaces) { gl.disable(gl.CULL_FACE); } else { gl.enable(gl.CULL_FACE); } frameCtx.backfaces = backfaces; } if (frameCtx.frontface !== frontface) { if (frontface) { gl.frontFace(gl.CCW); } else { gl.frontFace(gl.CW); } frameCtx.frontface = frontface; } gl.uniformMatrix4fv(this._uModelMatrix, false, mesh.worldMatrix); if (this._uClippable) { gl.uniform1i(this._uClippable, mesh._state.clippable); } gl.uniform3fv(this._uOffset, mesh._state.offset); if (this._uPositionsDecodeMatrix) { gl.uniformMatrix4fv(this._uPositionsDecodeMatrix, false, geometryState.positionsDecodeMatrix); this._aPosition.bindArrayBuffer(positionsBuf, geometryState.compressGeometry ? gl.UNSIGNED_SHORT : gl.FLOAT); } else { this._aPosition.bindArrayBuffer(positionsBuf); } gl.uniform2fv(this._uPickClipPos, frameCtx.pickClipPos); pickColorsBuf.bind(); gl.enableVertexAttribArray(this._aColor.location); gl.vertexAttribPointer(this._aColor.location, pickColorsBuf.itemSize, pickColorsBuf.itemType, true, 0, 0); // Normalize gl.drawArrays(geometryState.primitive, 0, positionsBuf.numItems / 3); }; PickTriangleRenderer.prototype._allocate = function (mesh) { const scene = mesh.scene; const gl = scene.canvas.gl; this._program = new Program(gl, this._shaderSource); this._useCount = 0; if (this._program.errors) { this.errors = this._program.errors; return; } const program = this._program; this._uPositionsDecodeMatrix = program.getLocation("positionsDecodeMatrix"); this._uModelMatrix = program.getLocation("modelMatrix"); this._uViewMatrix = program.getLocation("viewMatrix"); this._uProjMatrix = program.getLocation("projMatrix"); this._uSectionPlanes = []; const sectionPlanes = scene._sectionPlanesState.sectionPlanes; for (let i = 0, len = sectionPlanes.length; i < len; i++) { this._uSectionPlanes.push({ active: program.getLocation("sectionPlaneActive" + i), pos: program.getLocation("sectionPlanePos" + i), dir: program.getLocation("sectionPlaneDir" + i) }); } this._aPosition = program.getAttribute("position"); this._aColor = program.getAttribute("color"); this._uPickClipPos = program.getLocation("pickClipPos"); this._uClippable = program.getLocation("clippable"); this._uOffset = program.getLocation("offset"); if (scene.logarithmicDepthBufferEnabled ) { this._uLogDepthBufFC = program.getLocation("logDepthBufFC"); } }; /** * @author xeolabs / https://github.com/xeolabs */ /** * @private */ class OcclusionShaderSource { constructor(mesh) { this.vertex = buildVertex$1(mesh); this.fragment = buildFragment$1(mesh); } } function buildVertex$1(mesh) { const scene = mesh.scene; const sectionPlanesState = scene._sectionPlanesState; const clipping = sectionPlanesState.getNumAllocatedSectionPlanes() > 0; const quantizedGeometry = !!mesh._geometry._state.compressGeometry; const billboard = mesh._state.billboard; const stationary = mesh._state.stationary; const src = []; src.push('#version 300 es'); src.push("// Mesh occlusion vertex shader"); src.push("in vec3 position;"); src.push("uniform mat4 modelMatrix;"); src.push("uniform mat4 viewMatrix;"); src.push("uniform mat4 projMatrix;"); src.push("uniform vec3 offset;"); if (quantizedGeometry) { src.push("uniform mat4 positionsDecodeMatrix;"); } if (clipping) { src.push("out vec4 vWorldPosition;"); } if (scene.logarithmicDepthBufferEnabled) { src.push("uniform float logDepthBufFC;"); src.push("out float vFragDepth;"); src.push("bool isPerspectiveMatrix(mat4 m) {"); src.push(" return (m[2][3] == - 1.0);"); src.push("}"); src.push("out float isPerspective;"); } if (billboard === "spherical" || billboard === "cylindrical") { src.push("void billboard(inout mat4 mat) {"); src.push(" mat[0][0] = 1.0;"); src.push(" mat[0][1] = 0.0;"); src.push(" mat[0][2] = 0.0;"); if (billboard === "spherical") { src.push(" mat[1][0] = 0.0;"); src.push(" mat[1][1] = 1.0;"); src.push(" mat[1][2] = 0.0;"); } src.push(" mat[2][0] = 0.0;"); src.push(" mat[2][1] = 0.0;"); src.push(" mat[2][2] =1.0;"); src.push("}"); } src.push("void main(void) {"); src.push("vec4 localPosition = vec4(position, 1.0); "); src.push("vec4 worldPosition;"); if (quantizedGeometry) { src.push("localPosition = positionsDecodeMatrix * localPosition;"); } src.push("mat4 viewMatrix2 = viewMatrix;"); src.push("mat4 modelMatrix2 = modelMatrix;"); if (stationary) { src.push("viewMatrix2[3][0] = viewMatrix2[3][1] = viewMatrix2[3][2] = 0.0;"); } if (billboard === "spherical" || billboard === "cylindrical") { src.push("mat4 modelViewMatrix = viewMatrix2 * modelMatrix2;"); src.push("billboard(modelMatrix2);"); src.push("billboard(viewMatrix2);"); src.push("billboard(modelViewMatrix);"); src.push("worldPosition = modelMatrix2 * localPosition;"); src.push("worldPosition.xyz = worldPosition.xyz + offset;"); src.push("vec4 viewPosition = modelViewMatrix * localPosition;"); } else { src.push("worldPosition = modelMatrix2 * localPosition;"); src.push("worldPosition.xyz = worldPosition.xyz + offset;"); src.push("vec4 viewPosition = viewMatrix2 * worldPosition; "); } if (clipping) { src.push(" vWorldPosition = worldPosition;"); } src.push("vec4 clipPos = projMatrix * viewPosition;"); if (scene.logarithmicDepthBufferEnabled) { src.push("vFragDepth = 1.0 + clipPos.w;"); src.push("isPerspective = float (isPerspectiveMatrix(projMatrix));"); } src.push("gl_Position = clipPos;"); src.push("}"); return src; } function buildFragment$1(mesh) { const scene = mesh.scene; const sectionPlanesState = scene._sectionPlanesState; const clipping = sectionPlanesState.getNumAllocatedSectionPlanes() > 0; const src = []; src.push('#version 300 es'); src.push("// Mesh occlusion fragment shader"); src.push("#ifdef GL_FRAGMENT_PRECISION_HIGH"); src.push("precision highp float;"); src.push("precision highp int;"); src.push("#else"); src.push("precision mediump float;"); src.push("precision mediump int;"); src.push("#endif"); if (scene.logarithmicDepthBufferEnabled) { src.push("in float isPerspective;"); src.push("uniform float logDepthBufFC;"); src.push("in float vFragDepth;"); } if (clipping) { src.push("uniform bool clippable;"); src.push("in vec4 vWorldPosition;"); for (var i = 0; i < sectionPlanesState.getNumAllocatedSectionPlanes(); i++) { src.push("uniform bool sectionPlaneActive" + i + ";"); src.push("uniform vec3 sectionPlanePos" + i + ";"); src.push("uniform vec3 sectionPlaneDir" + i + ";"); } } src.push("out vec4 outColor;"); src.push("void main(void) {"); if (clipping) { src.push("if (clippable) {"); src.push(" float dist = 0.0;"); for (var i = 0; i < sectionPlanesState.getNumAllocatedSectionPlanes(); i++) { src.push("if (sectionPlaneActive" + i + ") {"); src.push(" dist += clamp(dot(-sectionPlaneDir" + i + ".xyz, vWorldPosition.xyz - sectionPlanePos" + i + ".xyz), 0.0, 1000.0);"); src.push("}"); } src.push(" if (dist > 0.0) { discard; }"); src.push("}"); } src.push(" outColor = vec4(0.0, 0.0, 1.0, 1.0); "); if (scene.logarithmicDepthBufferEnabled) { src.push("gl_FragDepth = isPerspective == 0.0 ? gl_FragCoord.z : log2( vFragDepth ) * logDepthBufFC * 0.5;"); } src.push("}"); return src; } /** * @author xeolabs / https://github.com/xeolabs */ const tempVec3a$G = math.vec3(); // No ID, because there is exactly one PickMeshRenderer per scene /** * @private */ const OcclusionRenderer = function (hash, mesh) { this._hash = hash; this._shaderSource = new OcclusionShaderSource(mesh); this._scene = mesh.scene; this._useCount = 0; this._allocate(mesh); }; const renderers$1 = {}; OcclusionRenderer.get = function (mesh) { const hash = [ mesh.scene.canvas.canvas.id, mesh.scene._sectionPlanesState.getHash(), mesh._geometry._state.hash, mesh._state.occlusionHash ].join(";"); let renderer = renderers$1[hash]; if (!renderer) { renderer = new OcclusionRenderer(hash, mesh); if (renderer.errors) { console.log(renderer.errors.join("\n")); return null; } renderers$1[hash] = renderer; stats.memory.programs++; } renderer._useCount++; return renderer; }; OcclusionRenderer.prototype.put = function () { if (--this._useCount === 0) { if (this._program) { this._program.destroy(); } delete renderers$1[this._hash]; stats.memory.programs--; } }; OcclusionRenderer.prototype.webglContextRestored = function () { this._program = null; }; OcclusionRenderer.prototype.drawMesh = function (frameCtx, mesh) { if (!this._program) { this._allocate(mesh); } const scene = this._scene; const gl = scene.canvas.gl; const materialState = mesh._material._state; const meshState = mesh._state; const geometryState = mesh._geometry._state; const origin = mesh.origin; if (materialState.alpha < 1.0) { return; } if (frameCtx.lastProgramId !== this._program.id) { frameCtx.lastProgramId = this._program.id; this._bindProgram(frameCtx); } if (materialState.id !== this._lastMaterialId) { const backfaces = materialState.backfaces; if (frameCtx.backfaces !== backfaces) { if (backfaces) { gl.disable(gl.CULL_FACE); } else { gl.enable(gl.CULL_FACE); } frameCtx.backfaces = backfaces; } const frontface = materialState.frontface; if (frameCtx.frontface !== frontface) { if (frontface) { gl.frontFace(gl.CCW); } else { gl.frontFace(gl.CW); } frameCtx.frontface = frontface; } this._lastMaterialId = materialState.id; } const camera = scene.camera; gl.uniformMatrix4fv(this._uViewMatrix, false, origin ? frameCtx.getRTCViewMatrix(meshState.originHash, origin) : camera.viewMatrix); if (meshState.clippable) { const numAllocatedSectionPlanes = scene._sectionPlanesState.getNumAllocatedSectionPlanes(); const numSectionPlanes = scene._sectionPlanesState.sectionPlanes.length; if (numAllocatedSectionPlanes > 0) { const sectionPlanes = scene._sectionPlanesState.sectionPlanes; const renderFlags = mesh.renderFlags; for (let sectionPlaneIndex = 0; sectionPlaneIndex < numAllocatedSectionPlanes; sectionPlaneIndex++) { const sectionPlaneUniforms = this._uSectionPlanes[sectionPlaneIndex]; if (sectionPlaneUniforms) { if (sectionPlaneIndex < numSectionPlanes) { const active = renderFlags.sectionPlanesActivePerLayer[sectionPlaneIndex]; gl.uniform1i(sectionPlaneUniforms.active, active ? 1 : 0); if (active) { const sectionPlane = sectionPlanes[sectionPlaneIndex]; if (origin) { const rtcSectionPlanePos = getPlaneRTCPos(sectionPlane.dist, sectionPlane.dir, origin, tempVec3a$G); gl.uniform3fv(sectionPlaneUniforms.pos, rtcSectionPlanePos); } else { gl.uniform3fv(sectionPlaneUniforms.pos, sectionPlane.pos); } gl.uniform3fv(sectionPlaneUniforms.dir, sectionPlane.dir); } } else { gl.uniform1i(sectionPlaneUniforms.active, 0); } } } } } gl.uniformMatrix4fv(this._uProjMatrix, false, camera._project._state.matrix); gl.uniformMatrix4fv(this._uModelMatrix, gl.FALSE, mesh.worldMatrix); if (this._uClippable) { gl.uniform1i(this._uClippable, mesh._state.clippable); } gl.uniform3fv(this._uOffset, mesh._state.offset); if (geometryState.id !== this._lastGeometryId) { if (this._uPositionsDecodeMatrix) { gl.uniformMatrix4fv(this._uPositionsDecodeMatrix, false, geometryState.positionsDecodeMatrix); } if (this._aPosition) { this._aPosition.bindArrayBuffer(geometryState.positionsBuf, geometryState.compressGeometry ? gl.UNSIGNED_SHORT : gl.FLOAT); frameCtx.bindArray++; } if (geometryState.indicesBuf) { geometryState.indicesBuf.bind(); frameCtx.bindArray++; } this._lastGeometryId = geometryState.id; } if (geometryState.indicesBuf) { gl.drawElements(geometryState.primitive, geometryState.indicesBuf.numItems, geometryState.indicesBuf.itemType, 0); frameCtx.drawElements++; } else if (geometryState.positions) { gl.drawArrays(gl.TRIANGLES, 0, geometryState.positions.numItems); } }; OcclusionRenderer.prototype._allocate = function (mesh) { const scene = mesh.scene; const gl = scene.canvas.gl; this._program = new Program(gl, this._shaderSource); if (this._program.errors) { this.errors = this._program.errors; return; } const program = this._program; this._uPositionsDecodeMatrix = program.getLocation("positionsDecodeMatrix"); this._uModelMatrix = program.getLocation("modelMatrix"); this._uViewMatrix = program.getLocation("viewMatrix"); this._uProjMatrix = program.getLocation("projMatrix"); this._uSectionPlanes = []; const clips = scene._sectionPlanesState.sectionPlanes; for (let i = 0, len = clips.length; i < len; i++) { this._uSectionPlanes.push({ active: program.getLocation("sectionPlaneActive" + i), pos: program.getLocation("sectionPlanePos" + i), dir: program.getLocation("sectionPlaneDir" + i) }); } this._aPosition = program.getAttribute("position"); this._uClippable = program.getLocation("clippable"); this._uOffset = program.getLocation("offset"); if (scene.logarithmicDepthBufferEnabled ) { this._uLogDepthBufFC = program.getLocation("logDepthBufFC"); } this._lastMaterialId = null; this._lastVertexBufsId = null; this._lastGeometryId = null; }; OcclusionRenderer.prototype._bindProgram = function (frameCtx) { const scene = this._scene; const project = scene.camera.project; const gl = scene.canvas.gl; this._program.bind(); frameCtx.useProgram++; gl.uniformMatrix4fv(this._uProjMatrix, false, project.matrix); if (scene.logarithmicDepthBufferEnabled ) { const logDepthBufFC = 2.0 / (Math.log(project.far + 1.0) / Math.LN2); gl.uniform1f(this._uLogDepthBufFC, logDepthBufFC); } this._lastMaterialId = null; this._lastVertexBufsId = null; this._lastGeometryId = null; }; /** * @private */ class ShadowShaderSource { constructor(mesh) { this.vertex = buildVertex(mesh); this.fragment = buildFragment(mesh); } } function buildVertex(mesh) { const scene = mesh.scene; const clipping = scene._sectionPlanesState.sectionPlanes.length > 0; const quantizedGeometry = !!mesh._geometry._state.compressGeometry; const src = []; src.push("// Mesh shadow vertex shader"); src.push("in vec3 position;"); src.push("uniform mat4 modelMatrix;"); src.push("uniform mat4 shadowViewMatrix;"); src.push("uniform mat4 shadowProjMatrix;"); src.push("uniform vec3 offset;"); if (quantizedGeometry) { src.push("uniform mat4 positionsDecodeMatrix;"); } if (clipping) { src.push("out vec4 vWorldPosition;"); } src.push("void main(void) {"); src.push("vec4 localPosition = vec4(position, 1.0); "); src.push("vec4 worldPosition;"); if (quantizedGeometry) { src.push("localPosition = positionsDecodeMatrix * localPosition;"); } src.push("worldPosition = modelMatrix * localPosition;"); src.push("worldPosition.xyz = worldPosition.xyz + offset;"); src.push("vec4 viewPosition = shadowViewMatrix * worldPosition; "); if (clipping) { src.push("vWorldPosition = worldPosition;"); } src.push(" gl_Position = shadowProjMatrix * viewPosition;"); src.push("}"); return src; } function buildFragment(mesh) { const scene = mesh.scene; scene.canvas.gl; const sectionPlanesState = scene._sectionPlanesState; const clipping = sectionPlanesState.getNumAllocatedSectionPlanes() > 0; const src = []; src.push("// Mesh shadow fragment shader"); src.push("#ifdef GL_FRAGMENT_PRECISION_HIGH"); src.push("precision highp float;"); src.push("precision highp int;"); src.push("#else"); src.push("precision mediump float;"); src.push("precision mediump int;"); src.push("#endif"); if (clipping) { src.push("uniform bool clippable;"); src.push("in vec4 vWorldPosition;"); for (var i = 0; i < sectionPlanesState.getNumAllocatedSectionPlanes(); i++) { src.push("uniform bool sectionPlaneActive" + i + ";"); src.push("uniform vec3 sectionPlanePos" + i + ";"); src.push("uniform vec3 sectionPlaneDir" + i + ";"); } } src.push("vec4 encodeFloat( const in float depth ) {"); src.push(" const vec4 bitShift = vec4(256 * 256 * 256, 256 * 256, 256, 1.0);"); src.push(" const vec4 bitMask = vec4(0, 1.0 / 256.0, 1.0 / 256.0, 1.0 / 256.0);"); src.push(" vec4 comp = fract(depth * bitShift);"); src.push(" comp -= comp.xxyz * bitMask;"); src.push(" return comp;"); src.push("}"); src.push("out vec4 outColor;"); src.push("void main(void) {"); if (clipping) { src.push("if (clippable) {"); src.push(" float dist = 0.0;"); for (var i = 0; i < sectionPlanesState.getNumAllocatedSectionPlanes(); i++) { src.push("if (sectionPlaneActive" + i + ") {"); src.push(" dist += clamp(dot(-sectionPlaneDir" + i + ".xyz, vWorldPosition.xyz - sectionPlanePos" + i + ".xyz), 0.0, 1000.0);"); src.push("}"); } src.push(" if (dist > 0.0) { discard; }"); src.push("}"); } src.push("outColor = encodeFloat(gl_FragCoord.z);"); src.push("}"); return src; } /** * @private */ const ShadowRenderer = function (hash, mesh) { this._hash = hash; this._shaderSource = new ShadowShaderSource(mesh); this._scene = mesh.scene; this._useCount = 0; this._allocate(mesh); }; const renderers = {}; ShadowRenderer.get = function (mesh) { const scene = mesh.scene; const hash = [scene.canvas.canvas.id, scene._sectionPlanesState.getHash(), mesh._geometry._state.hash, mesh._state.hash].join(";"); let renderer = renderers[hash]; if (!renderer) { renderer = new ShadowRenderer(hash, mesh); if (renderer.errors) { console.log(renderer.errors.join("\n")); return null; } renderers[hash] = renderer; stats.memory.programs++; } renderer._useCount++; return renderer; }; ShadowRenderer.prototype.put = function () { if (--this._useCount === 0) { if (this._program) { this._program.destroy(); } delete renderers[this._hash]; stats.memory.programs--; } }; ShadowRenderer.prototype.webglContextRestored = function () { this._program = null; }; ShadowRenderer.prototype.drawMesh = function (frame, mesh) { if (!this._program) { this._allocate(mesh); } const scene = this._scene; const gl = scene.canvas.gl; const materialState = mesh._material._state; const geometryState = mesh._geometry._state; if (frame.lastProgramId !== this._program.id) { frame.lastProgramId = this._program.id; this._bindProgram(frame); } if (materialState.id !== this._lastMaterialId) { const backfaces = materialState.backfaces; if (frame.backfaces !== backfaces) { if (backfaces) { gl.disable(gl.CULL_FACE); } else { gl.enable(gl.CULL_FACE); } frame.backfaces = backfaces; } const frontface = materialState.frontface; if (frame.frontface !== frontface) { if (frontface) { gl.frontFace(gl.CCW); } else { gl.frontFace(gl.CW); } frame.frontface = frontface; } if (frame.lineWidth !== materialState.lineWidth) { gl.lineWidth(materialState.lineWidth); frame.lineWidth = materialState.lineWidth; } if (this._uPointSize) { gl.uniform1i(this._uPointSize, materialState.pointSize); } this._lastMaterialId = materialState.id; } gl.uniformMatrix4fv(this._uModelMatrix, gl.FALSE, mesh.worldMatrix); if (geometryState.combineGeometry) { const vertexBufs = mesh.vertexBufs; if (vertexBufs.id !== this._lastVertexBufsId) { if (vertexBufs.positionsBuf && this._aPosition) { this._aPosition.bindArrayBuffer(vertexBufs.positionsBuf, vertexBufs.compressGeometry ? gl.UNSIGNED_SHORT : gl.FLOAT); frame.bindArray++; } this._lastVertexBufsId = vertexBufs.id; } } if (this._uClippable) { gl.uniform1i(this._uClippable, mesh._state.clippable); } gl.uniform3fv(this._uOffset, mesh._state.offset); if (geometryState.id !== this._lastGeometryId) { if (this._uPositionsDecodeMatrix) { gl.uniformMatrix4fv(this._uPositionsDecodeMatrix, false, geometryState.positionsDecodeMatrix); } if (geometryState.combineGeometry) { // VBOs were bound by the preceding VertexBufs chunk if (geometryState.indicesBufCombined) { geometryState.indicesBufCombined.bind(); frame.bindArray++; } } else { if (this._aPosition) { this._aPosition.bindArrayBuffer(geometryState.positionsBuf, geometryState.compressGeometry ? gl.UNSIGNED_SHORT : gl.FLOAT); frame.bindArray++; } if (geometryState.indicesBuf) { geometryState.indicesBuf.bind(); frame.bindArray++; } } this._lastGeometryId = geometryState.id; } if (geometryState.combineGeometry) { if (geometryState.indicesBufCombined) { gl.drawElements(geometryState.primitive, geometryState.indicesBufCombined.numItems, geometryState.indicesBufCombined.itemType, 0); frame.drawElements++; } } else { if (geometryState.indicesBuf) { gl.drawElements(geometryState.primitive, geometryState.indicesBuf.numItems, geometryState.indicesBuf.itemType, 0); frame.drawElements++; } else if (geometryState.positions) { gl.drawArrays(gl.TRIANGLES, 0, geometryState.positions.numItems); frame.drawArrays++; } } }; ShadowRenderer.prototype._allocate = function (mesh) { const scene = mesh.scene; const gl = scene.canvas.gl; this._program = new Program(gl, this._shaderSource); this._scene = scene; this._useCount = 0; if (this._program.errors) { this.errors = this._program.errors; return; } const program = this._program; this._uPositionsDecodeMatrix = program.getLocation("positionsDecodeMatrix"); this._uModelMatrix = program.getLocation("modelMatrix"); this._uShadowViewMatrix = program.getLocation("shadowViewMatrix"); this._uShadowProjMatrix = program.getLocation("shadowProjMatrix"); this._uSectionPlanes = {}; const clips = scene._sectionPlanesState.sectionPlanes; for (let i = 0, len = clips.length; i < len; i++) { this._uSectionPlanes.push({ active: program.getLocation("sectionPlaneActive" + i), pos: program.getLocation("sectionPlanePos" + i), dir: program.getLocation("sectionPlaneDir" + i) }); } this._aPosition = program.getAttribute("position"); this._uClippable = program.getLocation("clippable"); this._uOffset = program.getLocation("offset"); this._lastMaterialId = null; this._lastVertexBufsId = null; this._lastGeometryId = null; }; ShadowRenderer.prototype._bindProgram = function (frame) { if (!this._program) { this._allocate(mesh); } const scene = this._scene; const gl = scene.canvas.gl; const sectionPlanesState = scene._sectionPlanesState; this._program.bind(); frame.useProgram++; gl.uniformMatrix4fv(this._uShadowViewMatrix, false, frame.shadowViewMatrix); gl.uniformMatrix4fv(this._uShadowProjMatrix, false, frame.shadowProjMatrix); this._lastMaterialId = null; this._lastVertexBufsId = null; this._lastGeometryId = null; if (sectionPlanesState.getNumAllocatedSectionPlanes() > 0) { let sectionPlaneUniforms; let uSectionPlaneActive; let sectionPlane; let uSectionPlanePos; let uSectionPlaneDir; for (let i = 0, len = this._uSectionPlanes.length; i < len; i++) { sectionPlaneUniforms = this._uSectionPlanes[i]; uSectionPlaneActive = sectionPlaneUniforms.active; sectionPlane = sectionPlanesState.sectionPlanes[i]; if (uSectionPlaneActive) { gl.uniform1i(uSectionPlaneActive, sectionPlane.active); } uSectionPlanePos = sectionPlaneUniforms.pos; if (uSectionPlanePos) { gl.uniform3fv(sectionPlaneUniforms.pos, sectionPlane.pos); } uSectionPlaneDir = sectionPlaneUniforms.dir; if (uSectionPlaneDir) { gl.uniform3fv(sectionPlaneUniforms.dir, sectionPlane.dir); } } } }; /** * Indicates what rendering needs to be done for the layers within a {@link Drawable}. * * Each Drawable has a RenderFlags in {@link Drawable#renderFlags}. * * Before rendering each frame, {@link Renderer} will call {@link Drawable#rebuildRenderFlags} on each {@link Drawable}. * * Then, when rendering a frame, Renderer will apply rendering passes to each Drawable according on what flags are set in {@link Drawable#renderFlags}. * * @private */ class RenderFlags { /** * @private */ constructor() { /** * Set by {@link Drawable#rebuildRenderFlags} to indicate which layers are visible within the {@link Drawable}. * * This is a list of IDs of visible layers within the {@link Drawable}. The IDs will be whatever the * {@link Drawable} uses to identify its layers, usually integers. * * @property visibleLayers * @type {Number[]} */ this.visibleLayers = []; /** * Set by {@link Drawable#rebuildRenderFlags} to indicate which {@link SectionPlane}s are active within each layer of the {@link Drawable}. * * Layout is as follows: * * ````[ * false, false, true, // Layer 0, SectionPlanes 0, 1, 2 * false, true, true, // Layer 1, SectionPlanes 0, 1, 2 * true, false, true // Layer 2, SectionPlanes 0, 1, 2 * ]```` * * @property sectionPlanesActivePerLayer * @type {Boolean[]} */ this.sectionPlanesActivePerLayer = []; this.reset(); } /** * @private */ reset() { /** * Set by {@link Drawable#rebuildRenderFlags} to indicate whether the {@link Drawable} is culled. * * When this is ````false````, then all of the other properties on ````RenderFlags```` will remain at their default values. * * @property culled * @type {Boolean} */ this.culled = false; /** * Set by {@link Drawable#rebuildRenderFlags} to indicate whether the {@link Drawable} is sliced by any {@link SectionPlane}s. * * @property sectioned * @type {Boolean} */ this.sectioned = false; /** * Set by {@link Drawable#rebuildRenderFlags} to indicate the number of layers within the {@link Drawable}. * * @property numLayers * @type {Number} */ this.numLayers = 0; /** * Set by {@link Drawable#rebuildRenderFlags} to indicate the number of visible layers within the {@link Drawable}. * * @property numVisibleLayers * @type {Number} */ this.numVisibleLayers = 0; /** * Set by {@link Drawable#rebuildRenderFlags} to indicate the {@link Drawable} needs {@link Drawable#drawColorOpaque}. * @property colorOpaque * @type {boolean} */ this.colorOpaque = false; /** * Set by {@link Drawable#rebuildRenderFlags} to indicate the {@link Drawable} needs {@link Drawable#drawColorTransparent}. * @property colorTransparent * @type {boolean} */ this.colorTransparent = false; /** * Set by {@link Drawable#rebuildRenderFlags} to indicate the {@link Drawable} needs {@link Drawable#drawEdgesColorOpaque}. * @property edgesOpaque * @type {boolean} */ this.edgesOpaque = false; /** * Set by {@link Drawable#rebuildRenderFlags} to indicate the {@link Drawable} needs {@link Drawable#drawEdgesColorTransparent}. * @property edgesTransparent * @type {boolean} */ this.edgesTransparent = false; /** * Set by {@link Drawable#rebuildRenderFlags} to indicate the {@link Drawable} needs an opaque {@link Drawable#drawSilhouetteXRayed}. * @property xrayedSilhouetteOpaque * @type {boolean} */ this.xrayedSilhouetteOpaque = false; /** * Set by {@link Drawable#rebuildRenderFlags} to indicate the {@link Drawable} needs an opaque {@link Drawable#drawEdgesXRayed}. * @property xrayedEdgesOpaque * @type {boolean} */ this.xrayedEdgesOpaque = false; /** * Set by {@link Drawable#rebuildRenderFlags} to indicate the {@link Drawable} needs a transparent {@link Drawable#drawSilhouetteXRayed}. * @property xrayedSilhouetteTransparent * @type {boolean} */ this.xrayedSilhouetteTransparent = false; /** * Set by {@link Drawable#rebuildRenderFlags} to indicate the {@link Drawable} needs a transparent {@link Drawable#drawEdgesXRayed}. * @property xrayedEdgesTransparent * @type {boolean} */ this.xrayedEdgesTransparent = false; /** * Set by {@link Drawable#rebuildRenderFlags} to indicate the {@link Drawable} needs an opaque {@link Drawable#drawSilhouetteHighlighted}. * @property highlightedSilhouetteOpaque * @type {boolean} */ this.highlightedSilhouetteOpaque = false; /** * Set by {@link Drawable#rebuildRenderFlags} to indicate the {@link Drawable} needs an opaque {@link Drawable#drawEdgesHighlighted}. * @property highlightedEdgesOpaque * @type {boolean} */ this.highlightedEdgesOpaque = false; /** * Set by {@link Drawable#rebuildRenderFlags} to indicate the {@link Drawable} needs a transparent {@link Drawable#drawSilhouetteHighlighted}. * @property highlightedSilhouetteTransparent * @type {boolean} */ this.highlightedSilhouetteTransparent = false; /** * Set by {@link Drawable#rebuildRenderFlags} to indicate the {@link Drawable} needs a transparent {@link Drawable#drawEdgesHighlighted}. * @property highlightedEdgesTransparent * @type {boolean} */ this.highlightedEdgesTransparent = false; /** * Set by {@link Drawable#rebuildRenderFlags} to indicate the {@link Drawable} needs an opaque {@link Drawable#drawSilhouetteSelected}. * @property selectedSilhouetteOpaque * @type {boolean} */ this.selectedSilhouetteOpaque = false; /** * Set by {@link Drawable#rebuildRenderFlags} to indicate the {@link Drawable} needs an opaque {@link Drawable#drawEdgesSelected}. * @property selectedEdgesOpaque * @type {boolean} */ this.selectedEdgesOpaque = false; /** * Set by {@link Drawable#rebuildRenderFlags} to indicate the {@link Drawable} needs a transparent {@link Drawable#drawSilhouetteSelected}. * @property selectedSilhouetteTransparent * @type {boolean} */ this.selectedSilhouetteTransparent = false; /** * Set by {@link Drawable#rebuildRenderFlags} to indicate the {@link Drawable} needs a transparent {@link Drawable#drawEdgesSelected}. * @property selectedEdgesTransparent * @type {boolean} */ this.selectedEdgesTransparent = false; } } /** Fired when this Mesh is picked via a call to {@link Scene/pick:method"}}Scene#pick(){{/crossLink}}. The event parameters will be the hit result returned by the {@link Scene/pick:method"}}Scene#pick(){{/crossLink}} method. @event picked */ const obb = math.OBB3(); const angleAxis$1 = math.vec4(); const q1$1 = math.vec4(); const q2$1 = math.vec4(); const xAxis$1 = math.vec3([1, 0, 0]); const yAxis$1 = math.vec3([0, 1, 0]); const zAxis$1 = math.vec3([0, 0, 1]); const veca = math.vec3(3); const vecb = math.vec3(3); const identityMat$1 = math.identityMat4(); /** * @desc An {@link Entity} that is a drawable element, with a {@link Geometry} and a {@link Material}, that can be * connected into a scene graph using {@link Node}s. * * ## Usage * * The example below is the same as the one given for {@link Node}, since the two classes work together. In this example, * we'll create a scene graph in which a root {@link Node} represents a group and the Meshes are leaves. * * Since {@link Node} implements {@link Entity}, we can designate the root {@link Node} as a model, causing it to be registered by its * ID in {@link Scene#models}. * * Since Mesh also implements {@link Entity}, we can designate the leaf Meshes as objects, causing them to * be registered by their IDs in {@link Scene#objects}. * * We can then find those {@link Entity} types in {@link Scene#models} and {@link Scene#objects}. * * We can also update properties of our object-Meshes via calls to {@link Scene#setObjectsHighlighted} etc. * * [[Run this example](https://xeokit.github.io/xeokit-sdk/examples/scenegraph/#sceneGraph)] * * ````javascript * import {Viewer, Mesh, Node, PhongMaterial, buildBoxGeometry, ReadableGeometry} from "xeokit-sdk.es.js"; * * const viewer = new Viewer({ * canvasId: "myCanvas" * }); * * viewer.scene.camera.eye = [-21.80, 4.01, 6.56]; * viewer.scene.camera.look = [0, -5.75, 0]; * viewer.scene.camera.up = [0.37, 0.91, -0.11]; * * const boxGeometry = new ReadableGeometry(viewer.scene, buildBoxGeometry({ * xSize: 1, * ySize: 1, * zSize: 1 * })); * * new Node(viewer.scene, { * id: "table", * isModel: true, // <---------- Node represents a model, so is registered by ID in viewer.scene.models * rotation: [0, 50, 0], * position: [0, 0, 0], * scale: [1, 1, 1], * * children: [ * * new Mesh(viewer.scene, { // Red table leg * id: "redLeg", * isObject: true, // <------ Node represents an object, so is registered by ID in viewer.scene.objects * position: [-4, -6, -4], * scale: [1, 3, 1], * rotation: [0, 0, 0], * material: new PhongMaterial(viewer.scene, { * diffuse: [1, 0.3, 0.3] * }), * geometry: boxGeometry * }), * * new Mesh(viewer.scene, { // Green table leg * id: "greenLeg", * isObject: true, // <------ Node represents an object, so is registered by ID in viewer.scene.objects * position: [4, -6, -4], * scale: [1, 3, 1], * rotation: [0, 0, 0], * material: new PhongMaterial(viewer.scene, { * diffuse: [0.3, 1.0, 0.3] * }), * geometry: boxGeometry * }), * * new Mesh(viewer.scene, {// Blue table leg * id: "blueLeg", * isObject: true, // <------ Node represents an object, so is registered by ID in viewer.scene.objects * position: [4, -6, 4], * scale: [1, 3, 1], * rotation: [0, 0, 0], * material: new PhongMaterial(viewer.scene, { * diffuse: [0.3, 0.3, 1.0] * }), * geometry: boxGeometry * }), * * new Mesh(viewer.scene, { // Yellow table leg * id: "yellowLeg", * isObject: true, // <------ Node represents an object, so is registered by ID in viewer.scene.objects * position: [-4, -6, 4], * scale: [1, 3, 1], * rotation: [0, 0, 0], * material: new PhongMaterial(viewer.scene, { * diffuse: [1.0, 1.0, 0.0] * }), * geometry: boxGeometry * }), * * new Mesh(viewer.scene, { // Purple table top * id: "tableTop", * isObject: true, // <------ Node represents an object, so is registered by ID in viewer.scene.objects * position: [0, -3, 0], * scale: [6, 0.5, 6], * rotation: [0, 0, 0], * material: new PhongMaterial(viewer.scene, { * diffuse: [1.0, 0.3, 1.0] * }), * geometry: boxGeometry * }) * ] * }); * * // Find Nodes and Meshes by their IDs * * var table = viewer.scene.models["table"]; // Since table Node has isModel == true * * var redLeg = viewer.scene.objects["redLeg"]; // Since the Meshes have isObject == true * var greenLeg = viewer.scene.objects["greenLeg"]; * var blueLeg = viewer.scene.objects["blueLeg"]; * * // Highlight one of the table leg Meshes * * viewer.scene.setObjectsHighlighted(["redLeg"], true); // Since the Meshes have isObject == true * * // Periodically update transforms on our Nodes and Meshes * * viewer.scene.on("tick", function () { * * // Rotate legs * redLeg.rotateY(0.5); * greenLeg.rotateY(0.5); * blueLeg.rotateY(0.5); * * // Rotate table * table.rotateY(0.5); * table.rotateX(0.3); * }); * ```` * * ## Metadata * * As mentioned, we can also associate {@link MetaModel}s and {@link MetaObject}s with our {@link Node}s and Meshes, * within a {@link MetaScene}. See {@link MetaScene} for an example. * * @implements {Entity} * @implements {Drawable} */ class Mesh extends Component { /** * @constructor * @param {Component} owner Owner component. When destroyed, the owner will destroy this component as well. * @param {*} [cfg] Configs * @param {String} [cfg.id] Optional ID, unique among all components in the parent scene, generated automatically when omitted. * @param {String} [cfg.originalSystemId] ID of the corresponding object within the originating system, if any. * @param {Boolean} [cfg.isModel] Specify ````true```` if this Mesh represents a model, in which case the Mesh will be registered by {@link Mesh#id} in {@link Scene#models} and may also have a corresponding {@link MetaModel} with matching {@link MetaModel#id}, registered by that ID in {@link MetaScene#metaModels}. * @param {Boolean} [cfg.isObject] Specify ````true```` if this Mesh represents an object, in which case the Mesh will be registered by {@link Mesh#id} in {@link Scene#objects} and may also have a corresponding {@link MetaObject} with matching {@link MetaObject#id}, registered by that ID in {@link MetaScene#metaObjects}. * @param {Node} [cfg.parent] The parent Node. * @param {Number[]} [cfg.origin] World-space origin for this Mesh. When this is given, then ````matrix````, ````position```` and ````geometry```` are all assumed to be relative to this center. * @param {Number[]} [cfg.rtcCenter] Deprecated - renamed to ````origin````. * @param {Number[]} [cfg.position=[0,0,0]] 3D position of this Mesh, relative to ````origin````. * @param {Number[]} [cfg.scale=[1,1,1]] Local scale. * @param {Number[]} [cfg.rotation=[0,0,0]] Local rotation, as Euler angles given in degrees, for each of the X, Y and Z axis. * @param {Number[]} [cfg.matrix=[1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1]] Local modelling transform matrix. Overrides the position, scale and rotation parameters. * @param {Number[]} [cfg.offset=[0,0,0]] World-space 3D translation offset. Translates the Mesh in World space, after modelling transforms. * @param {Boolean} [cfg.occluder=true] Indicates if the Mesh is able to occlude {@link Marker}s. * @param {Boolean} [cfg.visible=true] Indicates if the Mesh is initially visible. * @param {Boolean} [cfg.culled=false] Indicates if the Mesh is initially culled from view. * @param {Boolean} [cfg.pickable=true] Indicates if the Mesh is initially pickable. * @param {Boolean} [cfg.clippable=true] Indicates if the Mesh is initially clippable. * @param {Boolean} [cfg.collidable=true] Indicates if the Mesh is initially included in boundary calculations. * @param {Boolean} [cfg.castsShadow=true] Indicates if the Mesh initially casts shadows. * @param {Boolean} [cfg.receivesShadow=true] Indicates if the Mesh initially receives shadows. * @param {Boolean} [cfg.xrayed=false] Indicates if the Mesh is initially xrayed. * @param {Boolean} [cfg.highlighted=false] Indicates if the Mesh is initially highlighted. * @param {Boolean} [cfg.selected=false] Indicates if the Mesh is initially selected. * @param {Boolean} [cfg.edges=false] Indicates if the Mesh's edges are initially emphasized. * @param {Boolean} [cfg.background=false] Indicates if the Mesh should act as background, e.g., it can be used for a skybox. * @param {Number[]} [cfg.colorize=[1.0,1.0,1.0]] Mesh's initial RGB colorize color, multiplies by the rendered fragment colors. * @param {Number} [cfg.opacity=1.0] Mesh's initial opacity factor, multiplies by the rendered fragment alpha. * @param {String} [cfg.billboard="none"] Mesh's billboarding behaviour. Options are "none" for no billboarding, "spherical" to always directly face {@link Camera.eye}, rotating both vertically and horizontally, or "cylindrical" to face the {@link Camera#eye} while rotating only about its vertically axis (use that mode for things like trees on a landscape). * @param {Geometry} [cfg.geometry] {@link Geometry} to define the shape of this Mesh. Inherits {@link Scene#geometry} by default. * @param {Material} [cfg.material] {@link Material} to define the normal rendered appearance for this Mesh. Inherits {@link Scene#material} by default. * @param {EmphasisMaterial} [cfg.xrayMaterial] {@link EmphasisMaterial} to define the xrayed appearance for this Mesh. Inherits {@link Scene#xrayMaterial} by default. * @param {EmphasisMaterial} [cfg.highlightMaterial] {@link EmphasisMaterial} to define the xrayed appearance for this Mesh. Inherits {@link Scene#highlightMaterial} by default. * @param {EmphasisMaterial} [cfg.selectedMaterial] {@link EmphasisMaterial} to define the selected appearance for this Mesh. Inherits {@link Scene#selectedMaterial} by default. * @param {EmphasisMaterial} [cfg.edgeMaterial] {@link EdgeMaterial} to define the appearance of enhanced edges for this Mesh. Inherits {@link Scene#edgeMaterial} by default. * @param {Number} [cfg.renderOrder=0] Specifies the rendering order for this mESH. This is used to control the order in which * mESHES are drawn when they have transparent objects, to give control over the order in which those objects are blended within the transparent * render pass. */ constructor(owner, cfg = {}) { super(owner, cfg); this.renderOrder = cfg.renderOrder || 0; /** * ID of the corresponding object within the originating system, if any. * * @type {String} * @abstract */ this.originalSystemId = (cfg.originalSystemId || this.id); /** @private **/ this.renderFlags = new RenderFlags(); this._state = new RenderState({ // NOTE: Renderer gets modeling and normal matrices from Mesh#matrix and Mesh.#normalWorldMatrix visible: true, culled: false, pickable: null, clippable: null, collidable: null, occluder: (cfg.occluder !== false), castsShadow: null, receivesShadow: null, xrayed: false, highlighted: false, selected: false, edges: false, stationary: !!cfg.stationary, background: !!cfg.background, billboard: this._checkBillboard(cfg.billboard), layer: null, colorize: null, pickID: this.scene._renderer.getPickID(this), drawHash: "", pickHash: "", offset: math.vec3(), origin: null, originHash: null, isUI: cfg.isUI }); this._drawRenderer = null; this._shadowRenderer = null; this._emphasisFillRenderer = null; this._emphasisEdgesRenderer = null; this._pickMeshRenderer = null; this._pickTriangleRenderer = null; this._occlusionRenderer = null; this._geometry = cfg.geometry ? this._checkComponent2(["ReadableGeometry", "VBOGeometry"], cfg.geometry) : this.scene.geometry; this._material = cfg.material ? this._checkComponent2(["PhongMaterial", "MetallicMaterial", "SpecularMaterial", "LambertMaterial"], cfg.material) : this.scene.material; this._xrayMaterial = cfg.xrayMaterial ? this._checkComponent("EmphasisMaterial", cfg.xrayMaterial) : this.scene.xrayMaterial; this._highlightMaterial = cfg.highlightMaterial ? this._checkComponent("EmphasisMaterial", cfg.highlightMaterial) : this.scene.highlightMaterial; this._selectedMaterial = cfg.selectedMaterial ? this._checkComponent("EmphasisMaterial", cfg.selectedMaterial) : this.scene.selectedMaterial; this._edgeMaterial = cfg.edgeMaterial ? this._checkComponent("EdgeMaterial", cfg.edgeMaterial) : this.scene.edgeMaterial; this._parentNode = null; this._aabb = null; this._aabbDirty = true; this._numTriangles = (this._geometry ? this._geometry.numTriangles : 0); this.scene._aabbDirty = true; this._scale = math.vec3(); this._quaternion = math.identityQuaternion(); this._rotation = math.vec3(); this._position = math.vec3(); this._worldMatrix = math.identityMat4(); this._worldNormalMatrix = math.identityMat4(); this._localMatrixDirty = true; this._worldMatrixDirty = true; this._worldNormalMatrixDirty = true; const origin = cfg.origin || cfg.rtcCenter; if (origin) { this._state.origin = math.vec3(origin); this._state.originHash = origin.join(); } if (cfg.matrix) { this.matrix = cfg.matrix; } else { this.scale = cfg.scale; this.position = cfg.position; if (cfg.quaternion) ; else { this.rotation = cfg.rotation; } } this._isObject = cfg.isObject; if (this._isObject) { this.scene._registerObject(this); } this._isModel = cfg.isModel; if (this._isModel) { this.scene._registerModel(this); } this.visible = cfg.visible; this.culled = cfg.culled; this.pickable = cfg.pickable; this.clippable = cfg.clippable; this.collidable = cfg.collidable; this.castsShadow = cfg.castsShadow; this.receivesShadow = cfg.receivesShadow; this.xrayed = cfg.xrayed; this.highlighted = cfg.highlighted; this.selected = cfg.selected; this.edges = cfg.edges; this.layer = cfg.layer; this.colorize = cfg.colorize; this.opacity = cfg.opacity; this.offset = cfg.offset; if (cfg.parentId) { const parentNode = this.scene.components[cfg.parentId]; if (!parentNode) { this.error("Parent not found: '" + cfg.parentId + "'"); } else if (!parentNode.isNode) { this.error("Parent is not a Node: '" + cfg.parentId + "'"); } else { parentNode.addChild(this); } this._parentNode = parentNode; } else if (cfg.parent) { if (!cfg.parent.isNode) { this.error("Parent is not a Node"); } cfg.parent.addChild(this); this._parentNode = cfg.parent; } this.compile(); } /** @private */ get type() { return "Mesh"; } //------------------------------------------------------------------------------------------------------------------ // Mesh members //------------------------------------------------------------------------------------------------------------------ /** * Returns true to indicate that this Component is a Mesh. * @final * @type {Boolean} */ get isMesh() { return true; } /** * The parent Node. * * The parent Node may also be set by passing the Mesh to the parent's {@link Node#addChild} method. * * @type {Node} */ get parent() { return this._parentNode; } /** * Defines the shape of this Mesh. * * Set to {@link Scene#geometry} by default. * * @type {Geometry} */ get geometry() { return this._geometry; } /** * Defines the appearance of this Mesh when rendering normally, ie. when not xrayed, highlighted or selected. * * Set to {@link Scene#material} by default. * * @type {Material} */ get material() { return this._material; } /** * Gets the Mesh's local translation. * * Default value is ````[0,0,0]````. * * @type {Number[]} */ get position() { return this._position; } /** * Sets the Mesh's local translation. * * Default value is ````[0,0,0]````. * * @type {Number[]} */ set position(value) { this._position.set(value || [0, 0, 0]); this._setLocalMatrixDirty(); this._setAABBDirty(); this.glRedraw(); } /** * Gets the Mesh's local rotation, as Euler angles given in degrees, for each of the X, Y and Z axis. * * Default value is ````[0,0,0]````. * * @type {Number[]} */ get rotation() { return this._rotation; } /** * Sets the Mesh's local rotation, as Euler angles given in degrees, for each of the X, Y and Z axis. * * Default value is ````[0,0,0]````. * * @type {Number[]} */ set rotation(value) { this._rotation.set(value || [0, 0, 0]); math.eulerToQuaternion(this._rotation, "XYZ", this._quaternion); this._setLocalMatrixDirty(); this._setAABBDirty(); this.glRedraw(); } /** * Gets the Mesh's local rotation quaternion. * * Default value is ````[0,0,0,1]````. * * @type {Number[]} */ get quaternion() { return this._quaternion; } /** * Sets the Mesh's local rotation quaternion. * * Default value is ````[0,0,0,1]````. * * @type {Number[]} */ set quaternion(value) { this._quaternion.set(value || [0, 0, 0, 1]); math.quaternionToEuler(this._quaternion, "XYZ", this._rotation); this._setLocalMatrixDirty(); this._setAABBDirty(); this.glRedraw(); } /** * Gets the Mesh's local scale. * * Default value is ````[1,1,1]````. * * @type {Number[]} */ get scale() { return this._scale; } /** * Sets the Mesh's local scale. * * Default value is ````[1,1,1]````. * * @type {Number[]} */ set scale(value) { this._scale.set(value || [1, 1, 1]); this._setLocalMatrixDirty(); this._setAABBDirty(); this.glRedraw(); } /** * Gets the Mesh's local modeling transform matrix. * * Default value is ````[1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]````. * * @type {Number[]} */ get matrix() { if (this._localMatrixDirty) { if (!this.__localMatrix) { this.__localMatrix = math.identityMat4(); } math.composeMat4(this._position, this._quaternion, this._scale, this.__localMatrix); this._localMatrixDirty = false; } return this.__localMatrix; } /** * Sets the Mesh's local modeling transform matrix. * * Default value is ````[1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]````. * * @type {Number[]} */ set matrix(value) { if (!this.__localMatrix) { this.__localMatrix = math.identityMat4(); } this.__localMatrix.set(value || identityMat$1); math.decomposeMat4(this.__localMatrix, this._position, this._quaternion, this._scale); this._localMatrixDirty = false; this._setWorldMatrixDirty(); this._setAABBDirty(); this.glRedraw(); } /** * Gets the Mesh's World matrix. * * @property worldMatrix * @type {Number[]} */ get worldMatrix() { if (this._worldMatrixDirty) { this._buildWorldMatrix(); } return this._worldMatrix; } /** * Gets the Mesh's World normal matrix. * * @type {Number[]} */ get worldNormalMatrix() { if (this._worldNormalMatrixDirty) { this._buildWorldNormalMatrix(); } return this._worldNormalMatrix; } /** * Returns true to indicate that Mesh implements {@link Entity}. * * @returns {Boolean} */ get isEntity() { return true; } /** * Returns ````true```` if this Mesh represents a model. * * When this returns ````true````, the Mesh will be registered by {@link Mesh#id} in {@link Scene#models} and * may also have a corresponding {@link MetaModel}. * * @type {Boolean} */ get isModel() { return this._isModel; } /** * Returns ````true```` if this Mesh represents an object. * * When this returns ````true````, the Mesh will be registered by {@link Mesh#id} in {@link Scene#objects} and * may also have a corresponding {@link MetaObject}. * * @type {Boolean} */ get isObject() { return this._isObject; } /** * Returns ````true```` if this Mesh is an UI object. * * @type {Boolean} */ get isUI() { return this._state.isUI; } /** * Gets the Mesh's World-space 3D axis-aligned bounding box. * * Represented by a six-element Float64Array containing the min/max extents of the * axis-aligned volume, ie. ````[xmin, ymin,zmin,xmax,ymax, zmax]````. * * @type {Number[]} */ get aabb() { if (this._aabbDirty) { this._updateAABB(); } return this._aabb; } /** * Gets the 3D origin of the Mesh's {@link Geometry}'s vertex positions. * * When this is given, then {@link Mesh#matrix}, {@link Mesh#position} and {@link Mesh#geometry} are all assumed to be relative to this center position. * * @type {Float64Array} */ get origin() { return this._state.origin; } /** * Sets the 3D origin of the Mesh's {@link Geometry}'s vertex positions. * * When this is given, then {@link Mesh#matrix}, {@link Mesh#position} and {@link Mesh#geometry} are all assumed to be relative to this center position. * * @type {Float64Array} */ set origin(origin) { if (origin) { if (!this._state.origin) { this._state.origin = math.vec3(); } this._state.origin.set(origin); this._state.originHash = origin.join(); this._setAABBDirty(); this.scene._aabbDirty = true; } else { if (this._state.origin) { this._state.origin = null; this._state.originHash = null; this._setAABBDirty(); this.scene._aabbDirty = true; } } } /** * Gets the World-space origin for this Mesh. * * Deprecated and replaced by {@link Mesh#origin}. * * @deprecated * @type {Float64Array} */ get rtcCenter() { return this.origin; } /** * Sets the World-space origin for this Mesh. * * Deprecated and replaced by {@link Mesh#origin}. * * @deprecated * @type {Float64Array} */ set rtcCenter(rtcCenter) { this.origin = rtcCenter; } /** * The approximate number of triangles in this Mesh. * * @type {Number} */ get numTriangles() { return this._numTriangles; } /** * Gets if this Mesh is visible. * * Only rendered when {@link Mesh#visible} is ````true```` and {@link Mesh#culled} is ````false````. * * When {@link Mesh#isObject} and {@link Mesh#visible} are both ````true```` the Mesh will be * registered by {@link Mesh#id} in {@link Scene#visibleObjects}. * * @type {Boolean} */ get visible() { return this._state.visible; } /** * Sets if this Mesh is visible. * * Only rendered when {@link Mesh#visible} is ````true```` and {@link Mesh#culled} is ````false````. * * When {@link Mesh#isObject} and {@link Mesh#visible} are both ````true```` the Mesh will be * registered by {@link Mesh#id} in {@link Scene#visibleObjects}. * * @type {Boolean} */ set visible(visible) { visible = visible !== false; this._state.visible = visible; if (this._isObject) { this.scene._objectVisibilityUpdated(this, visible); } this.glRedraw(); } /** * Gets if this Mesh is xrayed. * * XRayed appearance is configured by the {@link EmphasisMaterial} referenced by {@link Mesh#xrayMaterial}. * * When {@link Mesh#isObject} and {@link Mesh#xrayed} are both ````true``` the Mesh will be * registered by {@link Mesh#id} in {@link Scene#xrayedObjects}. * * @type {Boolean} */ get xrayed() { return this._state.xrayed; } /** * Sets if this Mesh is xrayed. * * XRayed appearance is configured by the {@link EmphasisMaterial} referenced by {@link Mesh#xrayMaterial}. * * When {@link Mesh#isObject} and {@link Mesh#xrayed} are both ````true``` the Mesh will be * registered by {@link Mesh#id} in {@link Scene#xrayedObjects}. * * @type {Boolean} */ set xrayed(xrayed) { xrayed = !!xrayed; if (this._state.xrayed === xrayed) { return; } this._state.xrayed = xrayed; if (this._isObject) { this.scene._objectXRayedUpdated(this, xrayed); } this.glRedraw(); } /** * Gets if this Mesh is highlighted. * * Highlighted appearance is configured by the {@link EmphasisMaterial} referenced by {@link Mesh#highlightMaterial}. * * When {@link Mesh#isObject} and {@link Mesh#highlighted} are both ````true```` the Mesh will be * registered by {@link Mesh#id} in {@link Scene#highlightedObjects}. * * @type {Boolean} */ get highlighted() { return this._state.highlighted; } /** * Sets if this Mesh is highlighted. * * Highlighted appearance is configured by the {@link EmphasisMaterial} referenced by {@link Mesh#highlightMaterial}. * * When {@link Mesh#isObject} and {@link Mesh#highlighted} are both ````true```` the Mesh will be * registered by {@link Mesh#id} in {@link Scene#highlightedObjects}. * * @type {Boolean} */ set highlighted(highlighted) { highlighted = !!highlighted; if (highlighted === this._state.highlighted) { return; } this._state.highlighted = highlighted; if (this._isObject) { this.scene._objectHighlightedUpdated(this, highlighted); } this.glRedraw(); } /** * Gets if this Mesh is selected. * * Selected appearance is configured by the {@link EmphasisMaterial} referenced by {@link Mesh#selectedMaterial}. * * When {@link Mesh#isObject} and {@link Mesh#selected} are both ````true``` the Mesh will be * registered by {@link Mesh#id} in {@link Scene#selectedObjects}. * * @type {Boolean} */ get selected() { return this._state.selected; } /** * Sets if this Mesh is selected. * * Selected appearance is configured by the {@link EmphasisMaterial} referenced by {@link Mesh#selectedMaterial}. * * When {@link Mesh#isObject} and {@link Mesh#selected} are both ````true``` the Mesh will be * registered by {@link Mesh#id} in {@link Scene#selectedObjects}. * * @type {Boolean} */ set selected(selected) { selected = !!selected; if (selected === this._state.selected) { return; } this._state.selected = selected; if (this._isObject) { this.scene._objectSelectedUpdated(this, selected); } this.glRedraw(); } /** * Gets if this Mesh is edge-enhanced. * * Edge appearance is configured by the {@link EdgeMaterial} referenced by {@link Mesh#edgeMaterial}. * * @type {Boolean} */ get edges() { return this._state.edges; } /** * Sets if this Mesh is edge-enhanced. * * Edge appearance is configured by the {@link EdgeMaterial} referenced by {@link Mesh#edgeMaterial}. * * @type {Boolean} */ set edges(edges) { edges = !!edges; if (edges === this._state.edges) { return; } this._state.edges = edges; this.glRedraw(); } /** * Gets if this Mesh is culled. * * Only rendered when {@link Mesh#visible} is ````true```` and {@link Mesh#culled} is ````false````. * * @type {Boolean} */ get culled() { return this._state.culled; } /** * Sets if this Mesh is culled. * * Only rendered when {@link Mesh#visible} is ````true```` and {@link Mesh#culled} is ````false````. * * @type {Boolean} */ set culled(value) { this._state.culled = !!value; this.glRedraw(); } /** * Gets if this Mesh is clippable. * * Clipping is done by the {@link SectionPlane}s in {@link Scene#sectionPlanes}. * * @type {Boolean} */ get clippable() { return this._state.clippable; } /** * Sets if this Mesh is clippable. * * Clipping is done by the {@link SectionPlane}s in {@link Scene#sectionPlanes}. * * @type {Boolean} */ set clippable(value) { value = value !== false; if (this._state.clippable === value) { return; } this._state.clippable = value; this.glRedraw(); } /** * Gets if this Mesh included in boundary calculations. * * @type {Boolean} */ get collidable() { return this._state.collidable; } /** * Sets if this Mesh included in boundary calculations. * * @type {Boolean} */ set collidable(value) { value = value !== false; if (value === this._state.collidable) { return; } this._state.collidable = value; this._setAABBDirty(); this.scene._aabbDirty = true; } //------------------------------------------------------------------------------------------------------------------ // Entity members //------------------------------------------------------------------------------------------------------------------ /** * Gets if this Mesh is pickable. * * Picking is done via calls to {@link Scene#pick}. * * @type {Boolean} */ get pickable() { return this._state.pickable; } /** * Sets if this Mesh is pickable. * * Picking is done via calls to {@link Scene#pick}. * * @type {Boolean} */ set pickable(value) { value = value !== false; if (this._state.pickable === value) { return; } this._state.pickable = value; // No need to trigger a render; // state is only used when picking } /** * Gets if this Mesh casts shadows. * * @type {Boolean} */ get castsShadow() { return this._state.castsShadow; } /** * Sets if this Mesh casts shadows. * * @type {Boolean} */ set castsShadow(value) { value = value !== false; if (value === this._state.castsShadow) { return; } this._state.castsShadow = value; this.glRedraw(); } /** * Gets if this Mesh can have shadows cast upon it. * * @type {Boolean} */ get receivesShadow() { return this._state.receivesShadow; } /** * Sets if this Mesh can have shadows cast upon it. * * @type {Boolean} */ set receivesShadow(value) { value = value !== false; if (value === this._state.receivesShadow) { return; } this._state.receivesShadow = value; this._state.hash = value ? "/mod/rs;" : "/mod;"; this.fire("dirty", this); // Now need to (re)compile objectRenderers to include/exclude shadow mapping } /** * Gets if this Mesh can have Scalable Ambient Obscurance (SAO) applied to it. * * SAO is configured by {@link SAO}. * * @type {Boolean} * @abstract */ get saoEnabled() { return false; // TODO: Support SAO on Meshes } /** * Gets the RGB colorize color for this Mesh. * * Multiplies by rendered fragment colors. * * Each element of the color is in range ````[0..1]````. * * @type {Number[]} */ get colorize() { return this._state.colorize; } /** * Sets the RGB colorize color for this Mesh. * * Multiplies by rendered fragment colors. * * Each element of the color is in range ````[0..1]````. * * @type {Number[]} */ set colorize(value) { let colorize = this._state.colorize; if (!colorize) { colorize = this._state.colorize = new Float32Array(4); colorize[3] = 1; } if (value) { colorize[0] = value[0]; colorize[1] = value[1]; colorize[2] = value[2]; } else { colorize[0] = 1; colorize[1] = 1; colorize[2] = 1; } const colorized = (!!value); this.scene._objectColorizeUpdated(this, colorized); this.glRedraw(); } /** * Gets the opacity factor for this Mesh. * * This is a factor in range ````[0..1]```` which multiplies by the rendered fragment alphas. * * @type {Number} */ get opacity() { return this._state.colorize[3]; } /** * Sets the opacity factor for this Mesh. * * This is a factor in range ````[0..1]```` which multiplies by the rendered fragment alphas. * * @type {Number} */ set opacity(opacity) { let colorize = this._state.colorize; if (!colorize) { colorize = this._state.colorize = new Float32Array(4); colorize[0] = 1; colorize[1] = 1; colorize[2] = 1; } const opacityUpdated = (opacity !== null && opacity !== undefined); colorize[3] = opacityUpdated ? opacity : 1.0; this.scene._objectOpacityUpdated(this, opacityUpdated); this.glRedraw(); } /** * Gets if this Mesh is transparent. * @returns {Boolean} */ get transparent() { return this._material.alphaMode === 2 /* blend */ || this._state.colorize[3] < 1 } /** * Gets the Mesh's rendering order relative to other Meshes. * * Default value is ````0````. * * This can be set on multiple transparent Meshes, to make them render in a specific order for correct alpha blending. * * @type {Number} */ get layer() { return this._state.layer; } /** * Sets the Mesh's rendering order relative to other Meshes. * * Default value is ````0````. * * This can be set on multiple transparent Meshes, to make them render in a specific order for correct alpha blending. * * @type {Number} */ set layer(value) { // TODO: Only accept rendering layer in range [0...MAX_layer] value = value || 0; value = Math.round(value); if (value === this._state.layer) { return; } this._state.layer = value; this._renderer.needStateSort(); } /** * Gets if the Node's position is stationary. * * When true, will disable the effect of {@link Camera} translations for this Mesh, while still allowing it to rotate. This is useful for skyboxes. * * @type {Boolean} */ get stationary() { return this._state.stationary; } /** * Gets the Node's billboarding behaviour. * * Options are: * * ````"none"```` - (default) - No billboarding. * * ````"spherical"```` - Mesh is billboarded to face the viewpoint, rotating both vertically and horizontally. * * ````"cylindrical"```` - Mesh is billboarded to face the viewpoint, rotating only about its vertically axis. Use this mode for things like trees on a landscape. * @type {String} */ get billboard() { return this._state.billboard; } /** * Gets the Mesh's 3D World-space offset. * * Default value is ````[0,0,0]````. * * @type {Number[]} */ get offset() { return this._state.offset; } /** * Sets the Mesh's 3D World-space offset. * * The offset dynamically translates the Mesh in World-space. * * Default value is ````[0, 0, 0]````. * * Provide a null or undefined value to reset to the default value. * * @type {Number[]} */ set offset(value) { this._state.offset.set(value || [0, 0, 0]); this._setAABBDirty(); this.glRedraw(); } /** * Returns true to indicate that Mesh implements {@link Drawable}. * @final * @type {Boolean} */ get isDrawable() { return true; } /** * Property with final value ````true```` to indicate that xeokit should render this Mesh in sorted order, relative to other Meshes. * * The sort order is determined by {@link Mesh#stateSortCompare}. * * Sorting is essential for rendering performance, so that xeokit is able to avoid applying runs of the same state changes to the GPU, ie. can collapse them. * * @type {Boolean} */ get isStateSortable() { return true; } /** * Defines the appearance of this Mesh when xrayed. * * Mesh is xrayed when {@link Mesh#xrayed} is ````true````. * * Set to {@link Scene#xrayMaterial} by default. * * @type {EmphasisMaterial} */ get xrayMaterial() { return this._xrayMaterial; } /** * Defines the appearance of this Mesh when highlighted. * * Mesh is xrayed when {@link Mesh#highlighted} is ````true````. * * Set to {@link Scene#highlightMaterial} by default. * * @type {EmphasisMaterial} */ get highlightMaterial() { return this._highlightMaterial; } /** * Defines the appearance of this Mesh when selected. * * Mesh is xrayed when {@link Mesh#selected} is ````true````. * * Set to {@link Scene#selectedMaterial} by default. * * @type {EmphasisMaterial} */ get selectedMaterial() { return this._selectedMaterial; } /** * Defines the appearance of this Mesh when edges are enhanced. * * Mesh is xrayed when {@link Mesh#edges} is ````true````. * * Set to {@link Scene#edgeMaterial} by default. * * @type {EdgeMaterial} */ get edgeMaterial() { return this._edgeMaterial; } _checkBillboard(value) { value = value || "none"; if (value !== "spherical" && value !== "cylindrical" && value !== "none") { this.error("Unsupported value for 'billboard': " + value + " - accepted values are " + "'spherical', 'cylindrical' and 'none' - defaulting to 'none'."); value = "none"; } return value; } /** * Called by xeokit to compile shaders for this Mesh. * @private */ compile() { const drawHash = this._makeDrawHash(); if (this._state.drawHash !== drawHash) { this._state.drawHash = drawHash; this._putDrawRenderers(); this._drawRenderer = DrawRenderer.get(this); // this._shadowRenderer = ShadowRenderer.get(this); this._emphasisFillRenderer = EmphasisFillRenderer.get(this); this._emphasisEdgesRenderer = EmphasisEdgesRenderer.get(this); } const pickHash = this._makePickHash(); if (this._state.pickHash !== pickHash) { this._state.pickHash = pickHash; this._putPickRenderers(); this._pickMeshRenderer = PickMeshRenderer.get(this); } if (this._state.occluder) { const occlusionHash = this._makeOcclusionHash(); if (this._state.occlusionHash !== occlusionHash) { this._state.occlusionHash = occlusionHash; this._putOcclusionRenderer(); this._occlusionRenderer = OcclusionRenderer.get(this); } } } _setLocalMatrixDirty() { this._localMatrixDirty = true; this._setWorldMatrixDirty(); } _setWorldMatrixDirty() { this._worldMatrixDirty = true; this._worldNormalMatrixDirty = true; } _buildWorldMatrix() { const localMatrix = this.matrix; if (!this._parentNode) { for (let i = 0, len = localMatrix.length; i < len; i++) { this._worldMatrix[i] = localMatrix[i]; } } else { math.mulMat4(this._parentNode.worldMatrix, localMatrix, this._worldMatrix); } this._worldMatrixDirty = false; } _buildWorldNormalMatrix() { if (this._worldMatrixDirty) { this._buildWorldMatrix(); } if (!this._worldNormalMatrix) { this._worldNormalMatrix = math.mat4(); } // Note: order of inverse and transpose doesn't matter math.transposeMat4(this._worldMatrix, this._worldNormalMatrix); math.inverseMat4(this._worldNormalMatrix); this._worldNormalMatrixDirty = false; } _setAABBDirty() { if (this.collidable) { for (let node = this; node; node = node._parentNode) { node._aabbDirty = true; } } } _updateAABB() { this.scene._aabbDirty = true; if (!this._aabb) { this._aabb = math.AABB3(); } this._buildAABB(this.worldMatrix, this._aabb); // Mesh or VBOSceneModel this._aabbDirty = false; } _webglContextRestored() { if (this._drawRenderer) { this._drawRenderer.webglContextRestored(); } if (this._shadowRenderer) { this._shadowRenderer.webglContextRestored(); } if (this._emphasisFillRenderer) { this._emphasisFillRenderer.webglContextRestored(); } if (this._emphasisEdgesRenderer) { this._emphasisEdgesRenderer.webglContextRestored(); } if (this._pickMeshRenderer) { this._pickMeshRenderer.webglContextRestored(); } if (this._pickTriangleRenderer) { this._pickMeshRenderer.webglContextRestored(); } if (this._occlusionRenderer) { this._occlusionRenderer.webglContextRestored(); } } _makeDrawHash() { const scene = this.scene; const hash = [ scene.canvas.canvas.id, (scene.gammaInput ? "gi;" : ";") + (scene.gammaOutput ? "go" : ""), scene._lightsState.getHash(), scene._sectionPlanesState.getHash() ]; const state = this._state; if (state.stationary) { hash.push("/s"); } if (state.billboard === "none") { hash.push("/n"); } else if (state.billboard === "spherical") { hash.push("/s"); } else if (state.billboard === "cylindrical") { hash.push("/c"); } if (state.receivesShadow) { hash.push("/rs"); } hash.push(";"); return hash.join(""); } _makePickHash() { const scene = this.scene; const hash = [ scene.canvas.canvas.id, scene._sectionPlanesState.getHash() ]; const state = this._state; if (state.stationary) { hash.push("/s"); } if (state.billboard === "none") { hash.push("/n"); } else if (state.billboard === "spherical") { hash.push("/s"); } else if (state.billboard === "cylindrical") { hash.push("/c"); } hash.push(";"); return hash.join(""); } _makeOcclusionHash() { const scene = this.scene; const hash = [ scene.canvas.canvas.id, scene._sectionPlanesState.getHash() ]; const state = this._state; if (state.stationary) { hash.push("/s"); } if (state.billboard === "none") { hash.push("/n"); } else if (state.billboard === "spherical") { hash.push("/s"); } else if (state.billboard === "cylindrical") { hash.push("/c"); } hash.push(";"); return hash.join(""); } _buildAABB(worldMatrix, aabb) { math.transformOBB3(worldMatrix, this._geometry.obb, obb); math.OBB3ToAABB3(obb, aabb); const offset = this._state.offset; aabb[0] += offset[0]; aabb[1] += offset[1]; aabb[2] += offset[2]; aabb[3] += offset[0]; aabb[4] += offset[1]; aabb[5] += offset[2]; if (this._state.origin) { const origin = this._state.origin; aabb[0] += origin[0]; aabb[1] += origin[1]; aabb[2] += origin[2]; aabb[3] += origin[0]; aabb[4] += origin[1]; aabb[5] += origin[2]; } } /** * Rotates the Mesh about the given local axis by the given increment. * * @param {Number[]} axis Local axis about which to rotate. * @param {Number} angle Angle increment in degrees. */ rotate(axis, angle) { angleAxis$1[0] = axis[0]; angleAxis$1[1] = axis[1]; angleAxis$1[2] = axis[2]; angleAxis$1[3] = angle * math.DEGTORAD; math.angleAxisToQuaternion(angleAxis$1, q1$1); math.mulQuaternions(this.quaternion, q1$1, q2$1); this.quaternion = q2$1; this._setLocalMatrixDirty(); this._setAABBDirty(); this.glRedraw(); return this; } /** * Rotates the Mesh about the given World-space axis by the given increment. * * @param {Number[]} axis Local axis about which to rotate. * @param {Number} angle Angle increment in degrees. */ rotateOnWorldAxis(axis, angle) { angleAxis$1[0] = axis[0]; angleAxis$1[1] = axis[1]; angleAxis$1[2] = axis[2]; angleAxis$1[3] = angle * math.DEGTORAD; math.angleAxisToQuaternion(angleAxis$1, q1$1); math.mulQuaternions(q1$1, this.quaternion, q1$1); //this.quaternion.premultiply(q1); return this; } /** * Rotates the Mesh about the local X-axis by the given increment. * * @param {Number} angle Angle increment in degrees. */ rotateX(angle) { return this.rotate(xAxis$1, angle); } /** * Rotates the Mesh about the local Y-axis by the given increment. * * @param {Number} angle Angle increment in degrees. */ rotateY(angle) { return this.rotate(yAxis$1, angle); } /** * Rotates the Mesh about the local Z-axis by the given increment. * * @param {Number} angle Angle increment in degrees. */ rotateZ(angle) { return this.rotate(zAxis$1, angle); } /** * Translates the Mesh along local space vector by the given increment. * * @param {Number[]} axis Normalized local space 3D vector along which to translate. * @param {Number} distance Distance to translate along the vector. */ translate(axis, distance) { math.vec3ApplyQuaternion(this.quaternion, axis, veca); math.mulVec3Scalar(veca, distance, vecb); math.addVec3(this.position, vecb, this.position); this._setLocalMatrixDirty(); this._setAABBDirty(); this.glRedraw(); return this; } //------------------------------------------------------------------------------------------------------------------ // Drawable members //------------------------------------------------------------------------------------------------------------------ /** * Translates the Mesh along the local X-axis by the given increment. * * @param {Number} distance Distance to translate along the X-axis. */ translateX(distance) { return this.translate(xAxis$1, distance); } /** * Translates the Mesh along the local Y-axis by the given increment. * * @param {Number} distance Distance to translate along the Y-axis. */ translateY(distance) { return this.translate(yAxis$1, distance); } /** * Translates the Mesh along the local Z-axis by the given increment. * * @param {Number} distance Distance to translate along the Z-axis. */ translateZ(distance) { return this.translate(zAxis$1, distance); } _putDrawRenderers() { if (this._drawRenderer) { this._drawRenderer.put(); this._drawRenderer = null; } if (this._shadowRenderer) { this._shadowRenderer.put(); this._shadowRenderer = null; } if (this._emphasisFillRenderer) { this._emphasisFillRenderer.put(); this._emphasisFillRenderer = null; } if (this._emphasisEdgesRenderer) { this._emphasisEdgesRenderer.put(); this._emphasisEdgesRenderer = null; } } _putPickRenderers() { if (this._pickMeshRenderer) { this._pickMeshRenderer.put(); this._pickMeshRenderer = null; } if (this._pickTriangleRenderer) { this._pickTriangleRenderer.put(); this._pickTriangleRenderer = null; } } _putOcclusionRenderer() { if (this._occlusionRenderer) { this._occlusionRenderer.put(); this._occlusionRenderer = null; } } /** * Comparison function used by the renderer to determine the order in which xeokit should render the Mesh, relative to to other Meshes. * * xeokit requires this method because Mesh implements {@link Drawable}. * * Sorting is essential for rendering performance, so that xeokit is able to avoid needlessly applying runs of the same rendering state changes to the GPU, ie. can collapse them. * * @param {Mesh} mesh1 * @param {Mesh} mesh2 * @returns {number} */ stateSortCompare(mesh1, mesh2) { return (mesh1._state.layer - mesh2._state.layer) || (mesh1._drawRenderer.id - mesh2._drawRenderer.id) // Program state || (mesh1._material._state.id - mesh2._material._state.id) // Material state || (mesh1._geometry._state.id - mesh2._geometry._state.id); // Geometry state } /** @private */ rebuildRenderFlags() { this.renderFlags.reset(); if (!this._getActiveSectionPlanes()) { this.renderFlags.culled = true; return; } this.renderFlags.numLayers = 1; this.renderFlags.numVisibleLayers = 1; this.renderFlags.visibleLayers[0] = 0; this._updateRenderFlags(); } /** * @private */ _updateRenderFlags() { const renderFlags = this.renderFlags; const state = this._state; if (state.xrayed) { const xrayMaterial = this._xrayMaterial._state; if (xrayMaterial.fill) { if (xrayMaterial.fillAlpha < 1.0) { renderFlags.xrayedSilhouetteTransparent = true; } else { renderFlags.xrayedSilhouetteOpaque = true; } } if (xrayMaterial.edges) { if (xrayMaterial.edgeAlpha < 1.0) { renderFlags.xrayedEdgesTransparent = true; } else { renderFlags.xrayedEdgesOpaque = true; } } } else { const normalMaterial = this._material._state; if (normalMaterial.alpha < 1.0 || state.colorize[3] < 1.0) { renderFlags.colorTransparent = true; } else { renderFlags.colorOpaque = true; } if (state.edges) { const edgeMaterial = this._edgeMaterial._state; if (edgeMaterial.alpha < 1.0) { renderFlags.edgesTransparent = true; } else { renderFlags.edgesOpaque = true; } } if (state.selected) { const selectedMaterial = this._selectedMaterial._state; if (selectedMaterial.fill) { if (selectedMaterial.fillAlpha < 1.0) { renderFlags.selectedSilhouetteTransparent = true; } else { renderFlags.selectedSilhouetteOpaque = true; } } if (selectedMaterial.edges) { if (selectedMaterial.edgeAlpha < 1.0) { renderFlags.selectedEdgesTransparent = true; } else { renderFlags.selectedEdgesOpaque = true; } } } else if (state.highlighted) { const highlightMaterial = this._highlightMaterial._state; if (highlightMaterial.fill) { if (highlightMaterial.fillAlpha < 1.0) { renderFlags.highlightedSilhouetteTransparent = true; } else { renderFlags.highlightedSilhouetteOpaque = true; } } if (highlightMaterial.edges) { if (highlightMaterial.edgeAlpha < 1.0) { renderFlags.highlightedEdgesTransparent = true; } else { renderFlags.highlightedEdgesOpaque = true; } } } } } _getActiveSectionPlanes() { if (this._state.clippable) { const sectionPlanes = this.scene._sectionPlanesState.sectionPlanes; const numSectionPlanes = sectionPlanes.length; if (numSectionPlanes > 0) { for (let i = 0; i < numSectionPlanes; i++) { const sectionPlane = sectionPlanes[i]; const renderFlags = this.renderFlags; if (!sectionPlane.active) { renderFlags.sectionPlanesActivePerLayer[i] = false; } else { if (this._state.origin) { const intersect = math.planeAABB3Intersect(sectionPlane.dir, sectionPlane.dist, this.aabb); const outside = (intersect === -1); if (outside) { return false; } const intersecting = (intersect === 0); renderFlags.sectionPlanesActivePerLayer[i] = intersecting; } else { renderFlags.sectionPlanesActivePerLayer[i] = true; } } } } } return true; } // ---------------------- NORMAL RENDERING ----------------------------------- /** @private */ drawColorOpaque(frameCtx) { if (this._drawRenderer || (this._drawRenderer = DrawRenderer.get(this))) { this._drawRenderer.drawMesh(frameCtx, this); } } /** @private */ drawColorTransparent(frameCtx) { if (this._drawRenderer || (this._drawRenderer = DrawRenderer.get(this))) { this._drawRenderer.drawMesh(frameCtx, this); } } // ---------------------- RENDERING SAO POST EFFECT TARGETS -------------- // TODO // ---------------------- EMPHASIS RENDERING ----------------------------------- /** @private */ drawSilhouetteXRayed(frameCtx) { if (this._emphasisFillRenderer || (this._emphasisFillRenderer = EmphasisFillRenderer.get(this))) { this._emphasisFillRenderer.drawMesh(frameCtx, this, 0); // 0 == xray } } /** @private */ drawSilhouetteHighlighted(frameCtx) { if (this._emphasisFillRenderer || (this._emphasisFillRenderer = EmphasisFillRenderer.get(this))) { this._emphasisFillRenderer.drawMesh(frameCtx, this, 1); // 1 == highlight } } /** @private */ drawSilhouetteSelected(frameCtx) { if (this._emphasisFillRenderer || (this._emphasisFillRenderer = EmphasisFillRenderer.get(this))) { this._emphasisFillRenderer.drawMesh(frameCtx, this, 2); // 2 == selected } } // ---------------------- EDGES RENDERING ----------------------------------- /** @private */ drawEdgesColorOpaque(frameCtx) { if (this._emphasisEdgesRenderer || (this._emphasisEdgesRenderer = EmphasisEdgesRenderer.get(this))) { this._emphasisEdgesRenderer.drawMesh(frameCtx, this, 3); // 3 == edges } } /** @private */ drawEdgesColorTransparent(frameCtx) { if (this._emphasisEdgesRenderer || (this._emphasisEdgesRenderer = EmphasisEdgesRenderer.get(this))) { this._emphasisEdgesRenderer.drawMesh(frameCtx, this, 3); // 3 == edges } } /** @private */ drawEdgesXRayed(frameCtx) { if (this._emphasisEdgesRenderer || (this._emphasisEdgesRenderer = EmphasisEdgesRenderer.get(this))) { this._emphasisEdgesRenderer.drawMesh(frameCtx, this, 0); // 0 == xray } } /** @private */ drawEdgesHighlighted(frameCtx) { if (this._emphasisEdgesRenderer || (this._emphasisEdgesRenderer = EmphasisEdgesRenderer.get(this))) { this._emphasisEdgesRenderer.drawMesh(frameCtx, this, 1); // 1 == highlight } } /** @private */ drawEdgesSelected(frameCtx) { if (this._emphasisEdgesRenderer || (this._emphasisEdgesRenderer = EmphasisEdgesRenderer.get(this))) { this._emphasisEdgesRenderer.drawMesh(frameCtx, this, 2); // 2 == selected } } // ---------------------- OCCLUSION CULL RENDERING ----------------------------------- /** @private */ drawOcclusion(frameCtx) { if (this._state.occluder && this._occlusionRenderer || (this._occlusionRenderer = OcclusionRenderer.get(this))) { this._occlusionRenderer.drawMesh(frameCtx, this); } } // ---------------------- SHADOW BUFFER RENDERING ----------------------------------- /** @private */ drawShadow(frameCtx) { if (this._shadowRenderer || (this._shadowRenderer = ShadowRenderer.get(this))) { this._shadowRenderer.drawMesh(frameCtx, this); } } // ---------------------- PICKING RENDERING ---------------------------------- /** @private */ drawPickMesh(frameCtx) { if (this._pickMeshRenderer || (this._pickMeshRenderer = PickMeshRenderer.get(this))) { this._pickMeshRenderer.drawMesh(frameCtx, this); } } /** @private */ canPickTriangle() { return this._geometry.isReadableGeometry; // VBOGeometry does not support surface picking because it has no geometry data in browser memory } /** @private */ drawPickTriangles(frameCtx) { if (this._pickTriangleRenderer || (this._pickTriangleRenderer = PickTriangleRenderer.get(this))) { this._pickTriangleRenderer.drawMesh(frameCtx, this); } } /** @private */ pickTriangleSurface(pickViewMatrix, pickProjMatrix, projection, pickResult) { pickTriangleSurface(this, pickViewMatrix, pickProjMatrix, projection, pickResult); } /** @private */ drawPickVertices(frameCtx) { } /** * @private * @returns {PerformanceNode} */ delegatePickedEntity() { return this; } //------------------------------------------------------------------------------------------------------------------ // Component members //------------------------------------------------------------------------------------------------------------------ /** * Destroys this Mesh. */ destroy() { super.destroy(); // xeokit.Object this._putDrawRenderers(); this._putPickRenderers(); this._putOcclusionRenderer(); this.scene._renderer.putPickID(this._state.pickID); // TODO: somehow puch this down into xeokit framework? if (this._isObject) { this.scene._deregisterObject(this); if (this._visible) { this.scene._objectVisibilityUpdated(this, false, false); } if (this._xrayed) { this.scene._objectXRayedUpdated(this, false, false); } if (this._selected) { this.scene._objectSelectedUpdated(this, false, false); } if (this._highlighted) { this.scene._objectHighlightedUpdated(this, false, false); } this.scene._objectColorizeUpdated(this, false); this.scene._objectOpacityUpdated(this, false); if (this.offset.some((v) => v !== 0)) this.scene._objectOffsetUpdated(this, false); } if (this._isModel) { this.scene._deregisterModel(this); } this.glRedraw(); } } const pickTriangleSurface = (function () { // Cached vars to avoid garbage collection const localRayOrigin = math.vec3(); const localRayDir = math.vec3(); const positionA = math.vec3(); const positionB = math.vec3(); const positionC = math.vec3(); const triangleVertices = math.vec3(); const position = math.vec4(); const worldPos = math.vec3(); const viewPos = math.vec3(); const bary = math.vec3(); const normalA = math.vec3(); const normalB = math.vec3(); const normalC = math.vec3(); const uva = math.vec3(); const uvb = math.vec3(); const uvc = math.vec3(); const tempVec4a = math.vec4(); const tempVec4b = math.vec4(); const tempVec4c = math.vec4(); const tempVec3 = math.vec3(); const tempVec3b = math.vec3(); const tempVec3c = math.vec3(); const tempVec3d = math.vec3(); const tempVec3e = math.vec3(); const tempVec3f = math.vec3(); const tempVec3g = math.vec3(); const tempVec3h = math.vec3(); const tempVec3i = math.vec3(); const tempVec3j = math.vec3(); const tempVec3k = math.vec3(); return function (mesh, pickViewMatrix, pickProjMatrix, projection, pickResult) { var primIndex = pickResult.primIndex; if (primIndex !== undefined && primIndex !== null && primIndex > -1) { const geometry = mesh.geometry._state; const scene = mesh.scene; const camera = scene.camera; const canvas = scene.canvas; if (geometry.primitiveName === "triangles") { // Triangle picked; this only happens when the // Mesh has a Geometry that has primitives of type "triangle" pickResult.primitive = "triangle"; // Get the World-space positions of the triangle's vertices const i = primIndex; // Indicates the first triangle index in the indices array const indices = geometry.indices; // Indices into geometry arrays, not into shared VertexBufs const positions = geometry.positions; let ia3; let ib3; let ic3; if (indices) { var ia = indices[i + 0]; var ib = indices[i + 1]; var ic = indices[i + 2]; triangleVertices[0] = ia; triangleVertices[1] = ib; triangleVertices[2] = ic; pickResult.indices = triangleVertices; ia3 = ia * 3; ib3 = ib * 3; ic3 = ic * 3; } else { ia3 = i * 3; ib3 = ia3 + 3; ic3 = ib3 + 3; } positionA[0] = positions[ia3 + 0]; positionA[1] = positions[ia3 + 1]; positionA[2] = positions[ia3 + 2]; positionB[0] = positions[ib3 + 0]; positionB[1] = positions[ib3 + 1]; positionB[2] = positions[ib3 + 2]; positionC[0] = positions[ic3 + 0]; positionC[1] = positions[ic3 + 1]; positionC[2] = positions[ic3 + 2]; if (geometry.compressGeometry) { // Decompress vertex positions const positionsDecodeMatrix = geometry.positionsDecodeMatrix; if (positionsDecodeMatrix) { geometryCompressionUtils.decompressPosition(positionA, positionsDecodeMatrix, positionA); geometryCompressionUtils.decompressPosition(positionB, positionsDecodeMatrix, positionB); geometryCompressionUtils.decompressPosition(positionC, positionsDecodeMatrix, positionC); } } // Attempt to ray-pick the triangle in local space if (pickResult.canvasPos) { math.canvasPosToLocalRay(canvas.canvas, mesh.origin ? createRTCViewMat(pickViewMatrix, mesh.origin) : pickViewMatrix, pickProjMatrix, projection, mesh.worldMatrix, pickResult.canvasPos, localRayOrigin, localRayDir); } else if (pickResult.origin && pickResult.direction) { math.worldRayToLocalRay(mesh.worldMatrix, pickResult.origin, pickResult.direction, localRayOrigin, localRayDir); } math.normalizeVec3(localRayDir); math.rayPlaneIntersect(localRayOrigin, localRayDir, positionA, positionB, positionC, position); // Get Local-space cartesian coordinates of the ray-triangle intersection pickResult.localPos = position; pickResult.position = position; // Get interpolated World-space coordinates // Need to transform homogeneous coords tempVec4a[0] = position[0]; tempVec4a[1] = position[1]; tempVec4a[2] = position[2]; tempVec4a[3] = 1; // Get World-space cartesian coordinates of the ray-triangle intersection math.transformVec4(mesh.worldMatrix, tempVec4a, tempVec4b); worldPos[0] = tempVec4b[0]; worldPos[1] = tempVec4b[1]; worldPos[2] = tempVec4b[2]; if (pickResult.canvasPos && mesh.origin) { worldPos[0] += mesh.origin[0]; worldPos[1] += mesh.origin[1]; worldPos[2] += mesh.origin[2]; } pickResult.worldPos = worldPos; // Get View-space cartesian coordinates of the ray-triangle intersection math.transformVec4(camera.matrix, tempVec4b, tempVec4c); viewPos[0] = tempVec4c[0]; viewPos[1] = tempVec4c[1]; viewPos[2] = tempVec4c[2]; pickResult.viewPos = viewPos; // Get barycentric coordinates of the ray-triangle intersection math.cartesianToBarycentric(position, positionA, positionB, positionC, bary); pickResult.bary = bary; // Get interpolated normal vector const normals = geometry.normals; if (normals) { if (geometry.compressGeometry) { // Decompress vertex normals const ia2 = ia * 3; const ib2 = ib * 3; const ic2 = ic * 3; geometryCompressionUtils.decompressNormal(normals.subarray(ia2, ia2 + 2), normalA); geometryCompressionUtils.decompressNormal(normals.subarray(ib2, ib2 + 2), normalB); geometryCompressionUtils.decompressNormal(normals.subarray(ic2, ic2 + 2), normalC); } else { normalA[0] = normals[ia3]; normalA[1] = normals[ia3 + 1]; normalA[2] = normals[ia3 + 2]; normalB[0] = normals[ib3]; normalB[1] = normals[ib3 + 1]; normalB[2] = normals[ib3 + 2]; normalC[0] = normals[ic3]; normalC[1] = normals[ic3 + 1]; normalC[2] = normals[ic3 + 2]; } const normal = math.addVec3(math.addVec3( math.mulVec3Scalar(normalA, bary[0], tempVec3), math.mulVec3Scalar(normalB, bary[1], tempVec3b), tempVec3c), math.mulVec3Scalar(normalC, bary[2], tempVec3d), tempVec3e); pickResult.worldNormal = math.normalizeVec3(math.transformVec3(mesh.worldNormalMatrix, normal, tempVec3f)); } // Get interpolated UV coordinates const uvs = geometry.uv; if (uvs) { uva[0] = uvs[(ia * 2)]; uva[1] = uvs[(ia * 2) + 1]; uvb[0] = uvs[(ib * 2)]; uvb[1] = uvs[(ib * 2) + 1]; uvc[0] = uvs[(ic * 2)]; uvc[1] = uvs[(ic * 2) + 1]; if (geometry.compressGeometry) { // Decompress vertex UVs const uvDecodeMatrix = geometry.uvDecodeMatrix; if (uvDecodeMatrix) { geometryCompressionUtils.decompressUV(uva, uvDecodeMatrix, uva); geometryCompressionUtils.decompressUV(uvb, uvDecodeMatrix, uvb); geometryCompressionUtils.decompressUV(uvc, uvDecodeMatrix, uvc); } } pickResult.uv = math.addVec3( math.addVec3( math.mulVec2Scalar(uva, bary[0], tempVec3g), math.mulVec2Scalar(uvb, bary[1], tempVec3h), tempVec3i), math.mulVec2Scalar(uvc, bary[2], tempVec3j), tempVec3k); } } } } })(); /** * @desc A **Material** defines the surface appearance of attached {@link Mesh}es. * * Material is the base class for: * * * {@link MetallicMaterial} - physically-based material for metallic surfaces. Use this one for things made of metal. * * {@link SpecularMaterial} - physically-based material for non-metallic (dielectric) surfaces. Use this one for insulators, such as ceramics, plastics, wood etc. * * {@link PhongMaterial} - material for classic Blinn-Phong shading. This is less demanding of graphics hardware than the physically-based materials. * * {@link LambertMaterial} - material for fast, flat-shaded CAD rendering without textures. Use this for navigating huge CAD or BIM models interactively. This material gives the best rendering performance and uses the least memory. * * {@link EmphasisMaterial} - defines the appearance of Meshes when "xrayed" or "highlighted". * * {@link EdgeMaterial} - defines the appearance of Meshes when edges are emphasized. * * A {@link Scene} is allowed to contain a mixture of these material types. * */ class Material extends Component { /** @private */ get type() { return "Material"; } constructor(owner, cfg={}) { super(owner, cfg); stats.memory.materials++; } destroy() { super.destroy(); stats.memory.materials--; } } const alphaModes$1 = {"opaque": 0, "mask": 1, "blend": 2}; const alphaModeNames$1 = ["opaque", "mask", "blend"]; /** * @desc Configures the normal rendered appearance of {@link Mesh}es using the non-physically-correct Blinn-Phong shading model. * * * Useful for non-realistic objects like gizmos. * * {@link SpecularMaterial} is best for insulators, such as wood, ceramics and plastic. * * {@link MetallicMaterial} is best for conductive materials, such as metal. * * {@link LambertMaterial} is appropriate for high-detail models that need to render as efficiently as possible. * * ## Usage * * In the example below, we'll create a {@link Mesh} with a PhongMaterial with a diffuse {@link Texture} and a specular {@link Fresnel}, using a {@link buildTorusGeometry} to create the {@link Geometry}. * * [[Run this example](/examples/index.html#materials_PhongMaterial)] * * ```` javascript * import {Viewer, Mesh, buildTorusGeometry, * ReadableGeometry, PhongMaterial, Texture, Fresnel} from "xeokit-sdk.es.js"; * * const viewer = new Viewer({ * canvasId: "myCanvas" * }); * * viewer.scene.camera.eye = [0, 0, 5]; * viewer.scene.camera.look = [0, 0, 0]; * viewer.scene.camera.up = [0, 1, 0]; * * new Mesh(viewer.scene, { * geometry: new ReadableGeometry(viewer.scene, buildTorusGeometry({ * center: [0, 0, 0], * radius: 1.5, * tube: 0.5, * radialSegments: 32, * tubeSegments: 24, * arc: Math.PI * 2.0 * }), * material: new PhongMaterial(viewer.scene, { * ambient: [0.9, 0.3, 0.9], * shininess: 30, * diffuseMap: new Texture(viewer.scene, { * src: "textures/diffuse/uvGrid2.jpg" * }), * specularFresnel: new Fresnel(viewer.scene, { * leftColor: [1.0, 1.0, 1.0], * rightColor: [0.0, 0.0, 0.0], * power: 4 * }) * }) * }); * ```` * * ## PhongMaterial Properties * * The following table summarizes PhongMaterial properties: * * | Property | Type | Range | Default Value | Space | Description | * |:--------:|:----:|:-----:|:-------------:|:-----:|:-----------:| * | {@link PhongMaterial#ambient} | Array | [0, 1] for all components | [1,1,1,1] | linear | The RGB components of the ambient light reflected by the material. | * | {@link PhongMaterial#diffuse} | Array | [0, 1] for all components | [1,1,1,1] | linear | The RGB components of the diffuse light reflected by the material. | * | {@link PhongMaterial#specular} | Array | [0, 1] for all components | [1,1,1,1] | linear | The RGB components of the specular light reflected by the material. | * | {@link PhongMaterial#emissive} | Array | [0, 1] for all components | [0,0,0] | linear | The RGB components of the light emitted by the material. | * | {@link PhongMaterial#alpha} | Number | [0, 1] | 1 | linear | The transparency of the material surface (0 fully transparent, 1 fully opaque). | * | {@link PhongMaterial#shininess} | Number | [0, 128] | 80 | linear | Determines the size and sharpness of specular highlights. | * | {@link PhongMaterial#reflectivity} | Number | [0, 1] | 1 | linear | Determines the amount of reflectivity. | * | {@link PhongMaterial#diffuseMap} | {@link Texture} | | null | sRGB | Texture RGB components multiplying by {@link PhongMaterial#diffuse}. If the fourth component (A) is present, it multiplies by {@link PhongMaterial#alpha}. | * | {@link PhongMaterial#specularMap} | {@link Texture} | | null | sRGB | Texture RGB components multiplying by {@link PhongMaterial#specular}. If the fourth component (A) is present, it multiplies by {@link PhongMaterial#alpha}. | * | {@link PhongMaterial#emissiveMap} | {@link Texture} | | null | linear | Texture with RGB components multiplying by {@link PhongMaterial#emissive}. | * | {@link PhongMaterial#alphaMap} | {@link Texture} | | null | linear | Texture with first component multiplying by {@link PhongMaterial#alpha}. | * | {@link PhongMaterial#occlusionMap} | {@link Texture} | | null | linear | Ambient occlusion texture multiplying by {@link PhongMaterial#ambient}, {@link PhongMaterial#diffuse} and {@link PhongMaterial#specular}. | * | {@link PhongMaterial#normalMap} | {@link Texture} | | null | linear | Tangent-space normal map. | * | {@link PhongMaterial#diffuseFresnel} | {@link Fresnel} | | null | | Fresnel term applied to {@link PhongMaterial#diffuse}. | * | {@link PhongMaterial#specularFresnel} | {@link Fresnel} | | null | | Fresnel term applied to {@link PhongMaterial#specular}. | * | {@link PhongMaterial#emissiveFresnel} | {@link Fresnel} | | null | | Fresnel term applied to {@link PhongMaterial#emissive}. | * | {@link PhongMaterial#reflectivityFresnel} | {@link Fresnel} | | null | | Fresnel term applied to {@link PhongMaterial#reflectivity}. | * | {@link PhongMaterial#alphaFresnel} | {@link Fresnel} | | null | | Fresnel term applied to {@link PhongMaterial#alpha}. | * | {@link PhongMaterial#lineWidth} | Number | [0..100] | 1 | | Line width in pixels. | * | {@link PhongMaterial#pointSize} | Number | [0..100] | 1 | | Point size in pixels. | * | {@link PhongMaterial#alphaMode} | String | "opaque", "blend", "mask" | "blend" | | Alpha blend mode. | * | {@link PhongMaterial#alphaCutoff} | Number | [0..1] | 0.5 | | Alpha cutoff value. | * | {@link PhongMaterial#backfaces} | Boolean | | false | | Whether to render geometry backfaces. | * | {@link PhongMaterial#frontface} | String | "ccw", "cw" | "ccw" | | The winding order for geometry frontfaces - "cw" for clockwise, or "ccw" for counter-clockwise. | */ class PhongMaterial extends Material { /** @private */ get type() { return "PhongMaterial"; } /** * @param {Component} owner Owner component. When destroyed, the owner will destroy this component as well. * @param {*} [cfg] The PhongMaterial configuration * @param {String} [cfg.id] Optional ID, unique among all components in the parent {@link Scene}, generated automatically when omitted. * @param {Number[]} [cfg.ambient=[1.0, 1.0, 1.0 ]] PhongMaterial ambient color. * @param {Number[]} [cfg.diffuse=[ 1.0, 1.0, 1.0 ]] PhongMaterial diffuse color. * @param {Number[]} [cfg.specular=[ 1.0, 1.0, 1.0 ]] PhongMaterial specular color. * @param {Number[]} [cfg.emissive=[ 0.0, 0.0, 0.0 ]] PhongMaterial emissive color. * @param {Number} [cfg.alpha=1] Scalar in range 0-1 that controls alpha, where 0 is completely transparent and 1 is completely opaque. * @param {Number} [cfg.shininess=80] Scalar in range 0-128 that determines the size and sharpness of specular highlights. * @param {Number} [cfg.reflectivity=1] Scalar in range 0-1 that controls how much {@link ReflectionMap} is reflected. * @param {Number} [cfg.lineWidth=1] Scalar that controls the width of lines. * @param {Number} [cfg.pointSize=1] Scalar that controls the size of points. * @param {Texture} [cfg.ambientMap=null] A ambient map {@link Texture}, which will multiply by the diffuse property. Must be within the same {@link Scene} as this PhongMaterial. * @param {Texture} [cfg.diffuseMap=null] A diffuse map {@link Texture}, which will override the effect of the diffuse property. Must be within the same {@link Scene} as this PhongMaterial. * @param {Texture} [cfg.specularMap=null] A specular map {@link Texture}, which will override the effect of the specular property. Must be within the same {@link Scene} as this PhongMaterial. * @param {Texture} [cfg.emissiveMap=undefined] An emissive map {@link Texture}, which will override the effect of the emissive property. Must be within the same {@link Scene} as this PhongMaterial. * @param {Texture} [cfg.normalMap=undefined] A normal map {@link Texture}. Must be within the same {@link Scene} as this PhongMaterial. * @param {Texture} [cfg.alphaMap=undefined] An alpha map {@link Texture}, which will override the effect of the alpha property. Must be within the same {@link Scene} as this PhongMaterial. * @param {Texture} [cfg.reflectivityMap=undefined] A reflectivity control map {@link Texture}, which will override the effect of the reflectivity property. Must be within the same {@link Scene} as this PhongMaterial. * @param {Texture} [cfg.occlusionMap=null] An occlusion map {@link Texture}. Must be within the same {@link Scene} as this PhongMaterial. * @param {Fresnel} [cfg.diffuseFresnel=undefined] A diffuse {@link Fresnel"}}Fresnel{{/crossLink}}. Must be within the same {@link Scene} as this PhongMaterial. * @param {Fresnel} [cfg.specularFresnel=undefined] A specular {@link Fresnel"}}Fresnel{{/crossLink}}. Must be within the same {@link Scene} as this PhongMaterial. * @param {Fresnel} [cfg.emissiveFresnel=undefined] An emissive {@link Fresnel"}}Fresnel{{/crossLink}}. Must be within the same {@link Scene} as this PhongMaterial. * @param {Fresnel} [cfg.alphaFresnel=undefined] An alpha {@link Fresnel"}}Fresnel{{/crossLink}}. Must be within the same {@link Scene} as this PhongMaterial. * @param {Fresnel} [cfg.reflectivityFresnel=undefined] A reflectivity {@link Fresnel"}}Fresnel{{/crossLink}}. Must be within the same {@link Scene} as this PhongMaterial. * @param {String} [cfg.alphaMode="opaque"] The alpha blend mode - accepted values are "opaque", "blend" and "mask". See the {@link PhongMaterial#alphaMode} property for more info. * @param {Number} [cfg.alphaCutoff=0.5] The alpha cutoff value. See the {@link PhongMaterial#alphaCutoff} property for more info. * @param {Boolean} [cfg.backfaces=false] Whether to render geometry backfaces. * @param {Boolean} [cfg.frontface="ccw"] The winding order for geometry front faces - "cw" for clockwise, or "ccw" for counter-clockwise. */ constructor(owner, cfg = {}) { super(owner, cfg); this._state = new RenderState({ type: "PhongMaterial", ambient: math.vec3([1.0, 1.0, 1.0]), diffuse: math.vec3([1.0, 1.0, 1.0]), specular: math.vec3([1.0, 1.0, 1.0]), emissive: math.vec3([0.0, 0.0, 0.0]), alpha: null, shininess: null, reflectivity: null, alphaMode: null, alphaCutoff: null, lineWidth: null, pointSize: null, backfaces: null, frontface: null, // Boolean for speed; true == "ccw", false == "cw" hash: null }); this.ambient = cfg.ambient; this.diffuse = cfg.diffuse; this.specular = cfg.specular; this.emissive = cfg.emissive; this.alpha = cfg.alpha; this.shininess = cfg.shininess; this.reflectivity = cfg.reflectivity; this.lineWidth = cfg.lineWidth; this.pointSize = cfg.pointSize; if (cfg.ambientMap) { this._ambientMap = this._checkComponent("Texture", cfg.ambientMap); } if (cfg.diffuseMap) { this._diffuseMap = this._checkComponent("Texture", cfg.diffuseMap); } if (cfg.specularMap) { this._specularMap = this._checkComponent("Texture", cfg.specularMap); } if (cfg.emissiveMap) { this._emissiveMap = this._checkComponent("Texture", cfg.emissiveMap); } if (cfg.alphaMap) { this._alphaMap = this._checkComponent("Texture", cfg.alphaMap); } if (cfg.reflectivityMap) { this._reflectivityMap = this._checkComponent("Texture", cfg.reflectivityMap); } if (cfg.normalMap) { this._normalMap = this._checkComponent("Texture", cfg.normalMap); } if (cfg.occlusionMap) { this._occlusionMap = this._checkComponent("Texture", cfg.occlusionMap); } if (cfg.diffuseFresnel) { this._diffuseFresnel = this._checkComponent("Fresnel", cfg.diffuseFresnel); } if (cfg.specularFresnel) { this._specularFresnel = this._checkComponent("Fresnel", cfg.specularFresnel); } if (cfg.emissiveFresnel) { this._emissiveFresnel = this._checkComponent("Fresnel", cfg.emissiveFresnel); } if (cfg.alphaFresnel) { this._alphaFresnel = this._checkComponent("Fresnel", cfg.alphaFresnel); } if (cfg.reflectivityFresnel) { this._reflectivityFresnel = this._checkComponent("Fresnel", cfg.reflectivityFresnel); } this.alphaMode = cfg.alphaMode; this.alphaCutoff = cfg.alphaCutoff; this.backfaces = cfg.backfaces; this.frontface = cfg.frontface; this._makeHash(); } _makeHash() { const state = this._state; const hash = ["/p"]; // 'P' for Phong if (this._normalMap) { hash.push("/nm"); if (this._normalMap.hasMatrix) { hash.push("/mat"); } } if (this._ambientMap) { hash.push("/am"); if (this._ambientMap.hasMatrix) { hash.push("/mat"); } hash.push("/" + this._ambientMap.encoding); } if (this._diffuseMap) { hash.push("/dm"); if (this._diffuseMap.hasMatrix) { hash.push("/mat"); } hash.push("/" + this._diffuseMap.encoding); } if (this._specularMap) { hash.push("/sm"); if (this._specularMap.hasMatrix) { hash.push("/mat"); } } if (this._emissiveMap) { hash.push("/em"); if (this._emissiveMap.hasMatrix) { hash.push("/mat"); } hash.push("/" + this._emissiveMap.encoding); } if (this._alphaMap) { hash.push("/opm"); if (this._alphaMap.hasMatrix) { hash.push("/mat"); } } if (this._reflectivityMap) { hash.push("/rm"); if (this._reflectivityMap.hasMatrix) { hash.push("/mat"); } } if (this._occlusionMap) { hash.push("/ocm"); if (this._occlusionMap.hasMatrix) { hash.push("/mat"); } } if (this._diffuseFresnel) { hash.push("/df"); } if (this._specularFresnel) { hash.push("/sf"); } if (this._emissiveFresnel) { hash.push("/ef"); } if (this._alphaFresnel) { hash.push("/of"); } if (this._reflectivityFresnel) { hash.push("/rf"); } hash.push(";"); state.hash = hash.join(""); } /** * Sets the PhongMaterial's ambient color. * * Default value is ````[0.3, 0.3, 0.3]````. * * @type {Number[]} */ set ambient(value) { let ambient = this._state.ambient; if (!ambient) { ambient = this._state.ambient = new Float32Array(3); } else if (value && ambient[0] === value[0] && ambient[1] === value[1] && ambient[2] === value[2]) { return; } if (value) { ambient[0] = value[0]; ambient[1] = value[1]; ambient[2] = value[2]; } else { ambient[0] = .2; ambient[1] = .2; ambient[2] = .2; } this.glRedraw(); } /** * Gets the PhongMaterial's ambient color. * * Default value is ````[0.3, 0.3, 0.3]````. * * @type {Number[]} */ get ambient() { return this._state.ambient; } /** * Sets the PhongMaterial's diffuse color. * * Multiplies by {@link PhongMaterial#diffuseMap}. * * Default value is ````[1.0, 1.0, 1.0]````. * * @type {Number[]} */ set diffuse(value) { let diffuse = this._state.diffuse; if (!diffuse) { diffuse = this._state.diffuse = new Float32Array(3); } else if (value && diffuse[0] === value[0] && diffuse[1] === value[1] && diffuse[2] === value[2]) { return; } if (value) { diffuse[0] = value[0]; diffuse[1] = value[1]; diffuse[2] = value[2]; } else { diffuse[0] = 1; diffuse[1] = 1; diffuse[2] = 1; } this.glRedraw(); } /** * Sets the PhongMaterial's diffuse color. * * Multiplies by {@link PhongMaterial#diffuseMap}. * * Default value is ````[1.0, 1.0, 1.0]````. * * @type {Number[]} */ get diffuse() { return this._state.diffuse; } /** * Sets the PhongMaterial's specular color. * * Multiplies by {@link PhongMaterial#specularMap}. * Default value is ````[1.0, 1.0, 1.0]````. * @type {Number[]} */ set specular(value) { let specular = this._state.specular; if (!specular) { specular = this._state.specular = new Float32Array(3); } else if (value && specular[0] === value[0] && specular[1] === value[1] && specular[2] === value[2]) { return; } if (value) { specular[0] = value[0]; specular[1] = value[1]; specular[2] = value[2]; } else { specular[0] = 1; specular[1] = 1; specular[2] = 1; } this.glRedraw(); } /** * Gets the PhongMaterial's specular color. * * Multiplies by {@link PhongMaterial#specularMap}. * Default value is ````[1.0, 1.0, 1.0]````. * @type {Number[]} */ get specular() { return this._state.specular; } /** * Sets the PhongMaterial's emissive color. * * Multiplies by {@link PhongMaterial#emissiveMap}. * * Default value is ````[0.0, 0.0, 0.0]````. * @type {Number[]} */ set emissive(value) { let emissive = this._state.emissive; if (!emissive) { emissive = this._state.emissive = new Float32Array(3); } else if (value && emissive[0] === value[0] && emissive[1] === value[1] && emissive[2] === value[2]) { return; } if (value) { emissive[0] = value[0]; emissive[1] = value[1]; emissive[2] = value[2]; } else { emissive[0] = 0; emissive[1] = 0; emissive[2] = 0; } this.glRedraw(); } /** * Gets the PhongMaterial's emissive color. * * Multiplies by {@link PhongMaterial#emissiveMap}. * * Default value is ````[0.0, 0.0, 0.0]````. * @type {Number[]} */ get emissive() { return this._state.emissive; } /** * Sets the PhongMaterial alpha. * * This is a factor in the range [0..1] indicating how transparent the PhongMaterial is. * * A value of 0.0 indicates fully transparent, 1.0 is fully opaque. * * Multiplies by {@link PhongMaterial#alphaMap}. * * Default value is ````1.0````. * * @type {Number} */ set alpha(value) { value = (value !== undefined && value !== null) ? value : 1.0; if (this._state.alpha === value) { return; } this._state.alpha = value; this.glRedraw(); } /** * Gets the PhongMaterial alpha. * * This is a factor in the range [0..1] indicating how transparent the PhongMaterial is. * * A value of 0.0 indicates fully transparent, 1.0 is fully opaque. * * Multiplies by {@link PhongMaterial#alphaMap}. * * Default value is ````1.0````. * * @type {Number} */ get alpha() { return this._state.alpha; } /** * Sets the PhongMaterial shininess. * * This is a factor in range [0..128] that determines the size and sharpness of the specular highlights create by this PhongMaterial. * * Larger values produce smaller, sharper highlights. A value of 0.0 gives very large highlights that are almost never * desirable. Try values close to 10 for a larger, fuzzier highlight and values of 100 or more for a small, sharp * highlight. * * Default value is ```` 80.0````. * * @type {Number} */ set shininess(value) { this._state.shininess = value !== undefined ? value : 80; this.glRedraw(); } /** * Gets the PhongMaterial shininess. * * This is a factor in range [0..128] that determines the size and sharpness of the specular highlights create by this PhongMaterial. * * Larger values produce smaller, sharper highlights. A value of 0.0 gives very large highlights that are almost never * desirable. Try values close to 10 for a larger, fuzzier highlight and values of 100 or more for a small, sharp * highlight. * * Default value is ```` 80.0````. * * @type {Number} */ get shininess() { return this._state.shininess; } /** * Sets the PhongMaterial's line width. * * This is not supported by WebGL implementations based on DirectX [2019]. * * Default value is ````1.0````. * * @type {Number} */ set lineWidth(value) { this._state.lineWidth = value || 1.0; this.glRedraw(); } /** * Gets the PhongMaterial's line width. * * This is not supported by WebGL implementations based on DirectX [2019]. * * Default value is ````1.0````. * * @type {Number} */ get lineWidth() { return this._state.lineWidth; } /** * Sets the PhongMaterial's point size. * * Default value is 1.0. * * @type {Number} */ set pointSize(value) { this._state.pointSize = value || 1.0; this.glRedraw(); } /** * Gets the PhongMaterial's point size. * * Default value is 1.0. * * @type {Number} */ get pointSize() { return this._state.pointSize; } /** * Sets how much {@link ReflectionMap} is reflected by this PhongMaterial. * * This is a scalar in range ````[0-1]````. Default value is ````1.0````. * * The surface will be non-reflective when this is ````0````, and completely mirror-like when it is ````1.0````. * * Multiplies by {@link PhongMaterial#reflectivityMap}. * * @type {Number} */ set reflectivity(value) { this._state.reflectivity = value !== undefined ? value : 1.0; this.glRedraw(); } /** * Gets how much {@link ReflectionMap} is reflected by this PhongMaterial. * * This is a scalar in range ````[0-1]````. Default value is ````1.0````. * * The surface will be non-reflective when this is ````0````, and completely mirror-like when it is ````1.0````. * * Multiplies by {@link PhongMaterial#reflectivityMap}. * * @type {Number} */ get reflectivity() { return this._state.reflectivity; } /** * Gets the PhongMaterials's normal map {@link Texture}. * * @type {Texture} */ get normalMap() { return this._normalMap; } /** * Gets the PhongMaterials's ambient {@link Texture}. * * Multiplies by {@link PhongMaterial#ambient}. * * @type {Texture} */ get ambientMap() { return this._ambientMap; } /** * Gets the PhongMaterials's diffuse {@link Texture}. * * Multiplies by {@link PhongMaterial#diffuse}. * * @type {Texture} */ get diffuseMap() { return this._diffuseMap; } /** * Gets the PhongMaterials's specular {@link Texture}. * * Multiplies by {@link PhongMaterial#specular}. * * @type {Texture} */ get specularMap() { return this._specularMap; } /** * Gets the PhongMaterials's emissive {@link Texture}. * * Multiplies by {@link PhongMaterial#emissive}. * * @type {Texture} */ get emissiveMap() { return this._emissiveMap; } /** * Gets the PhongMaterials's alpha {@link Texture}. * * Multiplies by {@link PhongMaterial#alpha}. * * @type {Texture} */ get alphaMap() { return this._alphaMap; } /** * Gets the PhongMaterials's reflectivity {@link Texture}. * * Multiplies by {@link PhongMaterial#reflectivity}. * * @type {Texture} */ get reflectivityMap() { return this._reflectivityMap; } /** * Gets the PhongMaterials's ambient occlusion {@link Texture}. * * @type {Texture} */ get occlusionMap() { return this._occlusionMap; } /** * Gets the PhongMaterials's diffuse {@link Fresnel}. * * Applies to {@link PhongMaterial#diffuse}. * * @type {Fresnel} */ get diffuseFresnel() { return this._diffuseFresnel; } /** * Gets the PhongMaterials's specular {@link Fresnel}. * * Applies to {@link PhongMaterial#specular}. * * @type {Fresnel} */ get specularFresnel() { return this._specularFresnel; } /** * Gets the PhongMaterials's emissive {@link Fresnel}. * * Applies to {@link PhongMaterial#emissive}. * * @type {Fresnel} */ get emissiveFresnel() { return this._emissiveFresnel; } /** * Gets the PhongMaterials's alpha {@link Fresnel}. * * Applies to {@link PhongMaterial#alpha}. * * @type {Fresnel} */ get alphaFresnel() { return this._alphaFresnel; } /** * Gets the PhongMaterials's reflectivity {@link Fresnel}. * * Applies to {@link PhongMaterial#reflectivity}. * * @type {Fresnel} */ get reflectivityFresnel() { return this._reflectivityFresnel; } /** * Sets the PhongMaterial's alpha rendering mode. * * This governs how alpha is treated. Alpha is the combined result of {@link PhongMaterial#alpha} and {@link PhongMaterial#alphaMap}. * * Supported values are: * * * "opaque" - The alpha value is ignored and the rendered output is fully opaque (default). * * "mask" - The rendered output is either fully opaque or fully transparent depending on the alpha value and the specified alpha cutoff value. * * "blend" - The alpha value is used to composite the source and destination areas. The rendered output is combined with the background using the normal painting operation (i.e. the Porter and Duff over operator). * *@type {String} */ set alphaMode(alphaMode) { alphaMode = alphaMode || "opaque"; let value = alphaModes$1[alphaMode]; if (value === undefined) { this.error("Unsupported value for 'alphaMode': " + alphaMode + " - defaulting to 'opaque'"); value = "opaque"; } if (this._state.alphaMode === value) { return; } this._state.alphaMode = value; this.glRedraw(); } /** * Gets the PhongMaterial's alpha rendering mode. * *@type {String} */ get alphaMode() { return alphaModeNames$1[this._state.alphaMode]; } /** * Sets the PhongMaterial's alpha cutoff value. * * This specifies the cutoff threshold when {@link PhongMaterial#alphaMode} equals "mask". If the alpha is greater than or equal to this value then it is rendered as fully * opaque, otherwise, it is rendered as fully transparent. A value greater than 1.0 will render the entire material as fully transparent. This value is ignored for other modes. * * Alpha is the combined result of {@link PhongMaterial#alpha} and {@link PhongMaterial#alphaMap}. * * Default value is ````0.5````. * * @type {Number} */ set alphaCutoff(alphaCutoff) { if (alphaCutoff === null || alphaCutoff === undefined) { alphaCutoff = 0.5; } if (this._state.alphaCutoff === alphaCutoff) { return; } this._state.alphaCutoff = alphaCutoff; } /** * Gets the PhongMaterial's alpha cutoff value. * * @type {Number} */ get alphaCutoff() { return this._state.alphaCutoff; } /** * Sets whether backfaces are visible on attached {@link Mesh}es. * * The backfaces will belong to {@link Geometry} compoents that are also attached to the {@link Mesh}es. * * Default is ````false````. * * @type {Boolean} */ set backfaces(value) { value = !!value; if (this._state.backfaces === value) { return; } this._state.backfaces = value; this.glRedraw(); } /** * Gets whether backfaces are visible on attached {@link Mesh}es. * * Default is ````false````. * * @type {Boolean} */ get backfaces() { return this._state.backfaces; } /** * Sets the winding direction of geometry front faces. * * Default is ````"ccw"````. * @type {String} */ set frontface(value) { value = value !== "cw"; if (this._state.frontface === value) { return; } this._state.frontface = value; this.glRedraw(); } /** * Gets the winding direction of front faces on attached {@link Mesh}es. * * Default is ````"ccw"````. * @type {String} */ get frontface() { return this._state.frontface ? "ccw" : "cw"; } /** * Destroys this PhongMaterial. */ destroy() { super.destroy(); this._state.destroy(); } } /** * @private */ function getExtension (gl, name) { if (gl._cachedExtensions === undefined) { gl._cachedExtensions = {}; } if (gl._cachedExtensions[name] !== undefined) { return gl._cachedExtensions[name]; } let extension; switch (name) { case 'WEBGL_depth_texture': extension = gl.getExtension('WEBGL_depth_texture') || gl.getExtension('MOZ_WEBGL_depth_texture') || gl.getExtension('WEBKIT_WEBGL_depth_texture'); break; case 'EXT_texture_filter_anisotropic': extension = gl.getExtension('EXT_texture_filter_anisotropic') || gl.getExtension('MOZ_EXT_texture_filter_anisotropic') || gl.getExtension('WEBKIT_EXT_texture_filter_anisotropic'); break; case 'WEBGL_compressed_texture_s3tc': extension = gl.getExtension('WEBGL_compressed_texture_s3tc') || gl.getExtension('MOZ_WEBGL_compressed_texture_s3tc') || gl.getExtension('WEBKIT_WEBGL_compressed_texture_s3tc'); break; case 'WEBGL_compressed_texture_pvrtc': extension = gl.getExtension('WEBGL_compressed_texture_pvrtc') || gl.getExtension('WEBKIT_WEBGL_compressed_texture_pvrtc'); break; default: extension = gl.getExtension(name); } gl._cachedExtensions[name] = extension; return extension; } /** * @private */ function convertConstant(gl, constantVal, encoding = null) { let extension; const p = constantVal; if (p === UnsignedByteType) return gl.UNSIGNED_BYTE; if (p === UnsignedShort4444Type) return gl.UNSIGNED_SHORT_4_4_4_4; if (p === UnsignedShort5551Type) return gl.UNSIGNED_SHORT_5_5_5_1; if (p === ByteType) return gl.BYTE; if (p === ShortType) return gl.SHORT; if (p === UnsignedShortType) return gl.UNSIGNED_SHORT; if (p === IntType) return gl.INT; if (p === UnsignedIntType) return gl.UNSIGNED_INT; if (p === FloatType) return gl.FLOAT; if (p === HalfFloatType) { return gl.HALF_FLOAT; } if (p === AlphaFormat) return gl.ALPHA; if (p === RGBAFormat) return gl.RGBA; if (p === LuminanceFormat) return gl.LUMINANCE; if (p === LuminanceAlphaFormat) return gl.LUMINANCE_ALPHA; if (p === DepthFormat) return gl.DEPTH_COMPONENT; if (p === DepthStencilFormat) return gl.DEPTH_STENCIL; if (p === RedFormat) return gl.RED; if (p === RGBFormat) { return gl.RGBA; } // WebGL2 formats. if (p === RedIntegerFormat) return gl.RED_INTEGER; if (p === RGFormat) return gl.RG; if (p === RGIntegerFormat) return gl.RG_INTEGER; if (p === RGBAIntegerFormat) return gl.RGBA_INTEGER; // S3TC if (p === RGB_S3TC_DXT1_Format || p === RGBA_S3TC_DXT1_Format || p === RGBA_S3TC_DXT3_Format || p === RGBA_S3TC_DXT5_Format) { if (encoding === sRGBEncoding) { const extension = getExtension(gl, 'WEBGL_compressed_texture_s3tc_srgb'); if (extension !== null) { if (p === RGB_S3TC_DXT1_Format) return extension.COMPRESSED_SRGB_S3TC_DXT1_EXT; if (p === RGBA_S3TC_DXT1_Format) return extension.COMPRESSED_SRGB_ALPHA_S3TC_DXT1_EXT; if (p === RGBA_S3TC_DXT3_Format) return extension.COMPRESSED_SRGB_ALPHA_S3TC_DXT3_EXT; if (p === RGBA_S3TC_DXT5_Format) return extension.COMPRESSED_SRGB_ALPHA_S3TC_DXT5_EXT; } else { return null; } } else { extension = getExtension(gl, 'WEBGL_compressed_texture_s3tc'); if (extension !== null) { if (p === RGB_S3TC_DXT1_Format) return extension.COMPRESSED_RGB_S3TC_DXT1_EXT; if (p === RGBA_S3TC_DXT1_Format) return extension.COMPRESSED_RGBA_S3TC_DXT1_EXT; if (p === RGBA_S3TC_DXT3_Format) return extension.COMPRESSED_RGBA_S3TC_DXT3_EXT; if (p === RGBA_S3TC_DXT5_Format) return extension.COMPRESSED_RGBA_S3TC_DXT5_EXT; } else { return null; } } } // PVRTC if (p === RGB_PVRTC_4BPPV1_Format || p === RGB_PVRTC_2BPPV1_Format || p === RGBA_PVRTC_4BPPV1_Format || p === RGBA_PVRTC_2BPPV1_Format) { const extension = getExtension(gl, 'WEBGL_compressed_texture_pvrtc'); if (extension !== null) { if (p === RGB_PVRTC_4BPPV1_Format) return extension.COMPRESSED_RGB_PVRTC_4BPPV1_IMG; if (p === RGB_PVRTC_2BPPV1_Format) return extension.COMPRESSED_RGB_PVRTC_2BPPV1_IMG; if (p === RGBA_PVRTC_4BPPV1_Format) return extension.COMPRESSED_RGBA_PVRTC_4BPPV1_IMG; if (p === RGBA_PVRTC_2BPPV1_Format) return extension.COMPRESSED_RGBA_PVRTC_2BPPV1_IMG; } else { return null; } } // ETC1 if (p === RGB_ETC1_Format) { const extension = getExtension(gl, 'WEBGL_compressed_texture_etc1'); if (extension !== null) { return extension.COMPRESSED_RGB_ETC1_WEBGL; } else { return null; } } // ETC2 if (p === RGB_ETC2_Format || p === RGBA_ETC2_EAC_Format) { const extension = getExtension(gl, 'WEBGL_compressed_texture_etc'); if (extension !== null) { if (p === RGB_ETC2_Format) return (encoding === sRGBEncoding) ? extension.COMPRESSED_SRGB8_ETC2 : extension.COMPRESSED_RGB8_ETC2; if (p === RGBA_ETC2_EAC_Format) return (encoding === sRGBEncoding) ? extension.COMPRESSED_SRGB8_ALPHA8_ETC2_EAC : extension.COMPRESSED_RGBA8_ETC2_EAC; } else { return null; } } // ASTC if (p === RGBA_ASTC_4x4_Format || p === RGBA_ASTC_5x4_Format || p === RGBA_ASTC_5x5_Format || p === RGBA_ASTC_6x5_Format || p === RGBA_ASTC_6x6_Format || p === RGBA_ASTC_8x5_Format || p === RGBA_ASTC_8x6_Format || p === RGBA_ASTC_8x8_Format || p === RGBA_ASTC_10x5_Format || p === RGBA_ASTC_10x6_Format || p === RGBA_ASTC_10x8_Format || p === RGBA_ASTC_10x10_Format || p === RGBA_ASTC_12x10_Format || p === RGBA_ASTC_12x12_Format) { const extension = getExtension(gl, 'WEBGL_compressed_texture_astc'); if (extension !== null) { if (p === RGBA_ASTC_4x4_Format) return (encoding === sRGBEncoding) ? extension.COMPRESSED_SRGB8_ALPHA8_ASTC_4x4_KHR : extension.COMPRESSED_RGBA_ASTC_4x4_KHR; if (p === RGBA_ASTC_5x4_Format) return (encoding === sRGBEncoding) ? extension.COMPRESSED_SRGB8_ALPHA8_ASTC_5x4_KHR : extension.COMPRESSED_RGBA_ASTC_5x4_KHR; if (p === RGBA_ASTC_5x5_Format) return (encoding === sRGBEncoding) ? extension.COMPRESSED_SRGB8_ALPHA8_ASTC_5x5_KHR : extension.COMPRESSED_RGBA_ASTC_5x5_KHR; if (p === RGBA_ASTC_6x5_Format) return (encoding === sRGBEncoding) ? extension.COMPRESSED_SRGB8_ALPHA8_ASTC_6x5_KHR : extension.COMPRESSED_RGBA_ASTC_6x5_KHR; if (p === RGBA_ASTC_6x6_Format) return (encoding === sRGBEncoding) ? extension.COMPRESSED_SRGB8_ALPHA8_ASTC_6x6_KHR : extension.COMPRESSED_RGBA_ASTC_6x6_KHR; if (p === RGBA_ASTC_8x5_Format) return (encoding === sRGBEncoding) ? extension.COMPRESSED_SRGB8_ALPHA8_ASTC_8x5_KHR : extension.COMPRESSED_RGBA_ASTC_8x5_KHR; if (p === RGBA_ASTC_8x6_Format) return (encoding === sRGBEncoding) ? extension.COMPRESSED_SRGB8_ALPHA8_ASTC_8x6_KHR : extension.COMPRESSED_RGBA_ASTC_8x6_KHR; if (p === RGBA_ASTC_8x8_Format) return (encoding === sRGBEncoding) ? extension.COMPRESSED_SRGB8_ALPHA8_ASTC_8x8_KHR : extension.COMPRESSED_RGBA_ASTC_8x8_KHR; if (p === RGBA_ASTC_10x5_Format) return (encoding === sRGBEncoding) ? extension.COMPRESSED_SRGB8_ALPHA8_ASTC_10x5_KHR : extension.COMPRESSED_RGBA_ASTC_10x5_KHR; if (p === RGBA_ASTC_10x6_Format) return (encoding === sRGBEncoding) ? extension.COMPRESSED_SRGB8_ALPHA8_ASTC_10x6_KHR : extension.COMPRESSED_RGBA_ASTC_10x6_KHR; if (p === RGBA_ASTC_10x8_Format) return (encoding === sRGBEncoding) ? extension.COMPRESSED_SRGB8_ALPHA8_ASTC_10x8_KHR : extension.COMPRESSED_RGBA_ASTC_10x8_KHR; if (p === RGBA_ASTC_10x10_Format) return (encoding === sRGBEncoding) ? extension.COMPRESSED_SRGB8_ALPHA8_ASTC_10x10_KHR : extension.COMPRESSED_RGBA_ASTC_10x10_KHR; if (p === RGBA_ASTC_12x10_Format) return (encoding === sRGBEncoding) ? extension.COMPRESSED_SRGB8_ALPHA8_ASTC_12x10_KHR : extension.COMPRESSED_RGBA_ASTC_12x10_KHR; if (p === RGBA_ASTC_12x12_Format) return (encoding === sRGBEncoding) ? extension.COMPRESSED_SRGB8_ALPHA8_ASTC_12x12_KHR : extension.COMPRESSED_RGBA_ASTC_12x12_KHR; } else { return null; } } // BPTC if (p === RGBA_BPTC_Format) { const extension = getExtension(gl, 'EXT_texture_compression_bptc'); if (extension !== null) { if (p === RGBA_BPTC_Format) { return (encoding === sRGBEncoding) ? extension.COMPRESSED_SRGB_ALPHA_BPTC_UNORM_EXT : extension.COMPRESSED_RGBA_BPTC_UNORM_EXT; } } else { return null; } } // if (p === UnsignedInt248Type) { return gl.UNSIGNED_INT_24_8; } if (p === RepeatWrapping) { return gl.REPEAT; } if (p === ClampToEdgeWrapping) { return gl.CLAMP_TO_EDGE; } if (p === NearestMipMapNearestFilter) { return gl.NEAREST_MIPMAP_LINEAR; } if (p === NearestMipMapLinearFilter) { return gl.NEAREST_MIPMAP_LINEAR; } if (p === LinearMipMapNearestFilter) { return gl.LINEAR_MIPMAP_NEAREST; } if (p === LinearMipMapLinearFilter) { return gl.LINEAR_MIPMAP_LINEAR; } if (p === NearestFilter) { return gl.NEAREST; } if (p === LinearFilter) { return gl.LINEAR; } return null; } const color$4 = new Uint8Array([0, 0, 0, 1]); /** * @desc A low-level component that represents a 2D WebGL texture. * * @private */ class Texture2D { constructor({gl, target, format, type, wrapS, wrapT, wrapR, encoding, preloadColor, premultiplyAlpha, flipY}) { this.gl = gl; this.target = target || gl.TEXTURE_2D; this.format = format || RGBAFormat; this.type = type || UnsignedByteType; this.internalFormat = null; this.premultiplyAlpha = !!premultiplyAlpha; this.flipY = !!flipY; this.unpackAlignment = 4; this.wrapS = wrapS || RepeatWrapping; this.wrapT = wrapT || RepeatWrapping; this.wrapR = wrapR || RepeatWrapping; this.encoding = encoding || sRGBEncoding; this.texture = gl.createTexture(); if (preloadColor) { this.setPreloadColor(preloadColor); // Prevents "there is no texture bound to the unit 0" error } this.allocated = true; } setPreloadColor(value) { if (!value) { color$4[0] = 0; color$4[1] = 0; color$4[2] = 0; color$4[3] = 255; } else { color$4[0] = Math.floor(value[0] * 255); color$4[1] = Math.floor(value[1] * 255); color$4[2] = Math.floor(value[2] * 255); color$4[3] = Math.floor((value[3] !== undefined ? value[3] : 1) * 255); } const gl = this.gl; gl.bindTexture(this.target, this.texture); if (this.target === gl.TEXTURE_CUBE_MAP) { const faces = [ gl.TEXTURE_CUBE_MAP_POSITIVE_X, gl.TEXTURE_CUBE_MAP_NEGATIVE_X, gl.TEXTURE_CUBE_MAP_POSITIVE_Y, gl.TEXTURE_CUBE_MAP_NEGATIVE_Y, gl.TEXTURE_CUBE_MAP_POSITIVE_Z, gl.TEXTURE_CUBE_MAP_NEGATIVE_Z ]; for (let i = 0, len = faces.length; i < len; i++) { gl.texImage2D(faces[i], 0, gl.RGBA, 1, 1, 0, gl.RGBA, gl.UNSIGNED_BYTE, color$4); } } else { gl.texImage2D(this.target, 0, gl.RGBA, 1, 1, 0, gl.RGBA, gl.UNSIGNED_BYTE, color$4); } gl.bindTexture(this.target, null); } setTarget(target) { this.target = target || this.gl.TEXTURE_2D; } setImage(image, props = {}) { const gl = this.gl; if (props.format !== undefined) { this.format = props.format; } if (props.internalFormat !== undefined) { this.internalFormat = props.internalFormat; } if (props.encoding !== undefined) { this.encoding = props.encoding; } if (props.type !== undefined) { this.type = props.type; } if (props.flipY !== undefined) { this.flipY = props.flipY; } if (props.premultiplyAlpha !== undefined) { this.premultiplyAlpha = props.premultiplyAlpha; } if (props.unpackAlignment !== undefined) { this.unpackAlignment = props.unpackAlignment; } if (props.minFilter !== undefined) { this.minFilter = props.minFilter; } if (props.magFilter !== undefined) { this.magFilter = props.magFilter; } if (props.wrapS !== undefined) { this.wrapS = props.wrapS; } if (props.wrapT !== undefined) { this.wrapT = props.wrapT; } if (props.wrapR !== undefined) { this.wrapR = props.wrapR; } let generateMipMap = false; gl.bindTexture(this.target, this.texture); const bak1 = gl.getParameter(gl.UNPACK_FLIP_Y_WEBGL); gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, this.flipY); const bak2 = gl.getParameter(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL); gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, this.premultiplyAlpha); const bak3 = gl.getParameter(gl.UNPACK_ALIGNMENT); gl.pixelStorei(gl.UNPACK_ALIGNMENT, this.unpackAlignment); const bak4 = gl.getParameter(gl.UNPACK_COLORSPACE_CONVERSION_WEBGL); gl.pixelStorei(gl.UNPACK_COLORSPACE_CONVERSION_WEBGL, gl.NONE); const minFilter = convertConstant(gl, this.minFilter); gl.texParameteri(this.target, gl.TEXTURE_MIN_FILTER, minFilter); if (minFilter === gl.NEAREST_MIPMAP_NEAREST || minFilter === gl.LINEAR_MIPMAP_NEAREST || minFilter === gl.NEAREST_MIPMAP_LINEAR || minFilter === gl.LINEAR_MIPMAP_LINEAR) { generateMipMap = true; } const magFilter = convertConstant(gl, this.magFilter); if (magFilter) { gl.texParameteri(this.target, gl.TEXTURE_MAG_FILTER, magFilter); } const wrapS = convertConstant(gl, this.wrapS); if (wrapS) { gl.texParameteri(this.target, gl.TEXTURE_WRAP_S, wrapS); } const wrapT = convertConstant(gl, this.wrapT); if (wrapT) { gl.texParameteri(this.target, gl.TEXTURE_WRAP_T, wrapT); } const glFormat = convertConstant(gl, this.format, this.encoding); const glType = convertConstant(gl, this.type); const glInternalFormat = getInternalFormat(gl, this.internalFormat, glFormat, glType, this.encoding, false); if (this.target === gl.TEXTURE_CUBE_MAP) { if (utils.isArray(image)) { const images = image; const faces = [ gl.TEXTURE_CUBE_MAP_POSITIVE_X, gl.TEXTURE_CUBE_MAP_NEGATIVE_X, gl.TEXTURE_CUBE_MAP_POSITIVE_Y, gl.TEXTURE_CUBE_MAP_NEGATIVE_Y, gl.TEXTURE_CUBE_MAP_POSITIVE_Z, gl.TEXTURE_CUBE_MAP_NEGATIVE_Z ]; for (let i = 0, len = faces.length; i < len; i++) { gl.texImage2D(faces[i], 0, glInternalFormat, glFormat, glType, images[i]); } } } else { gl.texImage2D(gl.TEXTURE_2D, 0, glInternalFormat, glFormat, glType, image); } if (generateMipMap) { gl.generateMipmap(this.target); } gl.bindTexture(this.target, null); gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, bak1); gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, bak2); gl.pixelStorei(gl.UNPACK_ALIGNMENT, bak3); gl.pixelStorei(gl.UNPACK_COLORSPACE_CONVERSION_WEBGL, bak4); } setCompressedData({mipmaps, props = {}}) { const gl = this.gl; const levels = mipmaps.length; // Cache props if (props.format !== undefined) { this.format = props.format; } if (props.internalFormat !== undefined) { this.internalFormat = props.internalFormat; } if (props.encoding !== undefined) { this.encoding = props.encoding; } if (props.type !== undefined) { this.type = props.type; } if (props.flipY !== undefined) { this.flipY = props.flipY; } if (props.premultiplyAlpha !== undefined) { this.premultiplyAlpha = props.premultiplyAlpha; } if (props.unpackAlignment !== undefined) { this.unpackAlignment = props.unpackAlignment; } if (props.minFilter !== undefined) { this.minFilter = props.minFilter; } if (props.magFilter !== undefined) { this.magFilter = props.magFilter; } if (props.wrapS !== undefined) { this.wrapS = props.wrapS; } if (props.wrapT !== undefined) { this.wrapT = props.wrapT; } if (props.wrapR !== undefined) { this.wrapR = props.wrapR; } gl.activeTexture(gl.TEXTURE0 + 0); gl.bindTexture(this.target, this.texture); let supportsMips = mipmaps.length > 1; gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, this.flipY); gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, this.premultiplyAlpha); gl.pixelStorei(gl.UNPACK_ALIGNMENT, this.unpackAlignment); gl.pixelStorei(gl.UNPACK_COLORSPACE_CONVERSION_WEBGL, gl.NONE); const wrapS = convertConstant(gl, this.wrapS); if (wrapS) { gl.texParameteri(this.target, gl.TEXTURE_WRAP_S, wrapS); } const wrapT = convertConstant(gl, this.wrapT); if (wrapT) { gl.texParameteri(this.target, gl.TEXTURE_WRAP_T, wrapT); } if (this.type === gl.TEXTURE_3D || this.type === gl.TEXTURE_2D_ARRAY) { const wrapR = convertConstant(gl, this.wrapR); if (wrapR) { gl.texParameteri(this.target, gl.TEXTURE_WRAP_R, wrapR); } gl.texParameteri(this.type, gl.TEXTURE_WRAP_R, wrapR); } if (supportsMips) { gl.texParameteri(this.target, gl.TEXTURE_MIN_FILTER, filterFallback(gl, this.minFilter)); gl.texParameteri(this.target, gl.TEXTURE_MAG_FILTER, filterFallback(gl, this.magFilter)); } else { gl.texParameteri(this.target, gl.TEXTURE_MIN_FILTER, convertConstant(gl, this.minFilter)); gl.texParameteri(this.target, gl.TEXTURE_MAG_FILTER, convertConstant(gl, this.magFilter)); } const glFormat = convertConstant(gl, this.format, this.encoding); const glType = convertConstant(gl, this.type); const glInternalFormat = getInternalFormat(gl, this.internalFormat, glFormat, glType, this.encoding, false); gl.texStorage2D(gl.TEXTURE_2D, levels, glInternalFormat, mipmaps[0].width, mipmaps[0].height); for (let i = 0, len = mipmaps.length; i < len; i++) { const mipmap = mipmaps[i]; if (this.format !== RGBAFormat) { if (glFormat !== null) { gl.compressedTexSubImage2D(gl.TEXTURE_2D, i, 0, 0, mipmap.width, mipmap.height, glFormat, mipmap.data); } else { console.warn('Attempt to load unsupported compressed texture format in .setCompressedData()'); } } else { gl.texSubImage2D(gl.TEXTURE_2D, i, 0, 0, mipmap.width, mipmap.height, glFormat, glType, mipmap.data); } } // if (generateMipMap) { // // gl.generateMipmap(this.target); // Only for roughness textures? // } gl.bindTexture(this.target, null); } setProps(props) { const gl = this.gl; gl.bindTexture(this.target, this.texture); this._uploadProps(props); gl.bindTexture(this.target, null); } _uploadProps(props) { const gl = this.gl; if (props.format !== undefined) { this.format = props.format; } if (props.internalFormat !== undefined) { this.internalFormat = props.internalFormat; } if (props.encoding !== undefined) { this.encoding = props.encoding; } if (props.type !== undefined) { this.type = props.type; } if (props.minFilter !== undefined) { const minFilter = convertConstant(gl, props.minFilter); if (minFilter) { this.minFilter = props.minFilter; gl.texParameteri(this.target, gl.TEXTURE_MIN_FILTER, minFilter); if (minFilter === gl.NEAREST_MIPMAP_NEAREST || minFilter === gl.LINEAR_MIPMAP_NEAREST || minFilter === gl.NEAREST_MIPMAP_LINEAR || minFilter === gl.LINEAR_MIPMAP_LINEAR) { gl.generateMipmap(this.target); } } } if (props.magFilter !== undefined) { const magFilter = convertConstant(gl, props.magFilter); if (magFilter) { this.magFilter = props.magFilter; gl.texParameteri(this.target, gl.TEXTURE_MAG_FILTER, magFilter); } } if (props.wrapS !== undefined) { const wrapS = convertConstant(gl, props.wrapS); if (wrapS) { this.wrapS = props.wrapS; gl.texParameteri(this.target, gl.TEXTURE_WRAP_S, wrapS); } } if (props.wrapT !== undefined) { const wrapT = convertConstant(gl, props.wrapT); if (wrapT) { this.wrapT = props.wrapT; gl.texParameteri(this.target, gl.TEXTURE_WRAP_T, wrapT); } } } bind(unit) { if (!this.allocated) { return; } if (this.texture) { const gl = this.gl; gl.activeTexture(gl["TEXTURE" + unit]); gl.bindTexture(this.target, this.texture); return true; } return false; } unbind(unit) { if (!this.allocated) { return; } if (this.texture) { const gl = this.gl; gl.activeTexture(gl["TEXTURE" + unit]); gl.bindTexture(this.target, null); } } destroy() { if (!this.allocated) { return; } if (this.texture) { this.gl.deleteTexture(this.texture); this.texture = null; } } } function getInternalFormat(gl, internalFormatName, glFormat, glType, encoding, isVideoTexture = false) { if (internalFormatName !== null) { if (gl[internalFormatName] !== undefined) { return gl[internalFormatName]; } console.warn('Attempt to use non-existing WebGL internal format \'' + internalFormatName + '\''); } let internalFormat = glFormat; if (glFormat === gl.RED) { if (glType === gl.FLOAT) internalFormat = gl.R32F; if (glType === gl.HALF_FLOAT) internalFormat = gl.R16F; if (glType === gl.UNSIGNED_BYTE) internalFormat = gl.R8; } if (glFormat === gl.RG) { if (glType === gl.FLOAT) internalFormat = gl.RG32F; if (glType === gl.HALF_FLOAT) internalFormat = gl.RG16F; if (glType === gl.UNSIGNED_BYTE) internalFormat = gl.RG8; } if (glFormat === gl.RGBA) { if (glType === gl.FLOAT) internalFormat = gl.RGBA32F; if (glType === gl.HALF_FLOAT) internalFormat = gl.RGBA16F; if (glType === gl.UNSIGNED_BYTE) internalFormat = (encoding === sRGBEncoding && isVideoTexture === false) ? gl.SRGB8_ALPHA8 : gl.RGBA8; if (glType === gl.UNSIGNED_SHORT_4_4_4_4) internalFormat = gl.RGBA4; if (glType === gl.UNSIGNED_SHORT_5_5_5_1) internalFormat = gl.RGB5_A1; } if (internalFormat === gl.R16F || internalFormat === gl.R32F || internalFormat === gl.RG16F || internalFormat === gl.RG32F || internalFormat === gl.RGBA16F || internalFormat === gl.RGBA32F) { getExtension(gl, 'EXT_color_buffer_float'); } return internalFormat; } function filterFallback(gl, f) { if (f === NearestFilter || f === NearestMipmapNearestFilter || f === NearestMipmapLinearFilter) { return gl.NEAREST; } return gl.LINEAR; } function ensureImageSizePowerOfTwo$1(image) { if (!isPowerOfTwo$1(image.width) || !isPowerOfTwo$1(image.height)) { const canvas = document.createElement("canvas"); canvas.width = nextHighestPowerOfTwo$1(image.width); canvas.height = nextHighestPowerOfTwo$1(image.height); const ctx = canvas.getContext("2d"); ctx.drawImage(image, 0, 0, image.width, image.height, 0, 0, canvas.width, canvas.height); image = canvas; } return image; } function isPowerOfTwo$1(x) { return (x & (x - 1)) === 0; } function nextHighestPowerOfTwo$1(x) { --x; for (let i = 1; i < 32; i <<= 1) { x = x | x >> i; } return x + 1; } /** * @desc A 2D texture map. * * * Textures are attached to {@link Material}s, which are attached to {@link Mesh}es. * * To create a Texture from an image file, set {@link Texture#src} to the image file path. * * To create a Texture from an HTMLImageElement, set the Texture's {@link Texture#image} to the HTMLImageElement. * * ## Usage * * In this example we have a Mesh with a {@link PhongMaterial} which applies diffuse {@link Texture}, and a {@link buildTorusGeometry} which builds a {@link ReadableGeometry}. * * Note that xeokit will ignore {@link PhongMaterial#diffuse} and {@link PhongMaterial#specular}, since we override those * with {@link PhongMaterial#diffuseMap} and {@link PhongMaterial#specularMap}. The {@link Texture} pixel colors directly * provide the diffuse and specular components for each fragment across the {@link ReadableGeometry} surface. * * [[Run this example](/examples/index.html#materials_Texture)] * * ```` javascript * import {Viewer, Mesh, buildTorusGeometry, * ReadableGeometry, PhongMaterial, Texture} from "xeokit-sdk.es.js"; * * const viewer = new Viewer({ * canvasId: "myCanvas" * }); * * viewer.camera.eye = [0, 0, 5]; * viewer.camera.look = [0, 0, 0]; * viewer.camera.up = [0, 1, 0]; * * new Mesh(viewer.scene, { * geometry: new ReadableGeometry(viewer.scene, buildTorusGeometry({ * center: [0, 0, 0], * radius: 1.5, * tube: 0.5, * radialSegments: 32, * tubeSegments: 24, * arc: Math.PI * 2.0 * }), * material: new PhongMaterial(viewer.scene, { * ambient: [0.9, 0.3, 0.9], * shininess: 30, * diffuseMap: new Texture(viewer.scene, { * src: "textures/diffuse/uvGrid2.jpg" * }) * }) * }); *```` */ class Texture extends Component { /** @private */ get type() { return "Texture"; } /** * @constructor * @param {Component} owner Owner component. When destroyed, the owner will destroy this Texture as well. * @param {*} [cfg] Configs * @param {String} [cfg.id] Optional ID for this Texture, unique among all components in the parent scene, generated automatically when omitted. * @param {String} [cfg.src=null] Path to image file to load into this Texture. See the {@link Texture#src} property for more info. * @param {HTMLImageElement} [cfg.image=null] HTML Image object to load into this Texture. See the {@link Texture#image} property for more info. * @param {Number} [cfg.minFilter=LinearMipmapLinearFilter] How the texture is sampled when a texel covers less than one pixel. * Supported values are {@link LinearMipmapLinearFilter}, {@link LinearMipMapNearestFilter}, {@link NearestMipMapNearestFilter}, {@link NearestMipMapLinearFilter} and {@link LinearMipMapLinearFilter}. * @param {Number} [cfg.magFilter=LinearFilter] How the texture is sampled when a texel covers more than one pixel. Supported values are {@link LinearFilter} and {@link NearestFilter}. * @param {Number} [cfg.wrapS=RepeatWrapping] Wrap parameter for texture coordinate *S*. Supported values are {@link ClampToEdgeWrapping}, {@link MirroredRepeatWrapping} and {@link RepeatWrapping}. * @param {Number} [cfg.wrapT=RepeatWrapping] Wrap parameter for texture coordinate *T*. Supported values are {@link ClampToEdgeWrapping}, {@link MirroredRepeatWrapping} and {@link RepeatWrapping}.. * @param {Boolean} [cfg.flipY=false] Flips this Texture's source data along its vertical axis when ````true````. * @param {Number} [cfg.encoding=LinearEncoding] Encoding format. Supported values are {@link LinearEncoding} and {@link sRGBEncoding}. * @param {Number[]} [cfg.translate=[0,0]] 2D translation vector that will be added to texture's *S* and *T* coordinates. * @param {Number[]} [cfg.scale=[1,1]] 2D scaling vector that will be applied to texture's *S* and *T* coordinates. * @param {Number} [cfg.rotate=0] Rotation, in degrees, that will be applied to texture's *S* and *T* coordinates. */ constructor(owner, cfg = {}) { super(owner, cfg); this._state = new RenderState({ texture: new Texture2D({gl: this.scene.canvas.gl}), matrix: math.identityMat4(), hasMatrix: (cfg.translate && (cfg.translate[0] !== 0 || cfg.translate[1] !== 0)) || (!!cfg.rotate) || (cfg.scale && (cfg.scale[0] !== 0 || cfg.scale[1] !== 0)), minFilter: this._checkMinFilter(cfg.minFilter), magFilter: this._checkMagFilter(cfg.magFilter), wrapS: this._checkWrapS(cfg.wrapS), wrapT: this._checkWrapT(cfg.wrapT), flipY: this._checkFlipY(cfg.flipY), encoding: this._checkEncoding(cfg.encoding) }); // Data source this._src = null; this._image = null; // Transformation this._translate = math.vec2([0, 0]); this._scale = math.vec2([1, 1]); this._rotate = math.vec2([0, 0]); this._matrixDirty = false; // Transform this.translate = cfg.translate; this.scale = cfg.scale; this.rotate = cfg.rotate; // Data source if (cfg.src) { this.src = cfg.src; // Image file } else if (cfg.image) { this.image = cfg.image; // Image object } stats.memory.textures++; } _checkMinFilter(value) { value = value || LinearMipMapLinearFilter; if (value !== LinearFilter && value !== LinearMipMapNearestFilter && value !== LinearMipMapLinearFilter && value !== NearestMipMapLinearFilter && value !== NearestMipMapNearestFilter) { this.error("Unsupported value for 'minFilter' - supported values are LinearFilter, LinearMipMapNearestFilter, NearestMipMapNearestFilter, " + "NearestMipMapLinearFilter and LinearMipMapLinearFilter. Defaulting to LinearMipMapLinearFilter."); value = LinearMipMapLinearFilter; } return value; } _checkMagFilter(value) { value = value || LinearFilter; if (value !== LinearFilter && value !== NearestFilter) { this.error("Unsupported value for 'magFilter' - supported values are LinearFilter and NearestFilter. Defaulting to LinearFilter."); value = LinearFilter; } return value; } _checkWrapS(value) { value = value || RepeatWrapping; if (value !== ClampToEdgeWrapping && value !== MirroredRepeatWrapping && value !== RepeatWrapping) { this.error("Unsupported value for 'wrapS' - supported values are ClampToEdgeWrapping, MirroredRepeatWrapping and RepeatWrapping. Defaulting to RepeatWrapping."); value = RepeatWrapping; } return value; } _checkWrapT(value) { value = value || RepeatWrapping; if (value !== ClampToEdgeWrapping && value !== MirroredRepeatWrapping && value !== RepeatWrapping) { this.error("Unsupported value for 'wrapT' - supported values are ClampToEdgeWrapping, MirroredRepeatWrapping and RepeatWrapping. Defaulting to RepeatWrapping."); value = RepeatWrapping; } return value; } _checkFlipY(value) { return !!value; } _checkEncoding(value) { value = value || LinearEncoding; if (value !== LinearEncoding && value !== sRGBEncoding) { this.error("Unsupported value for 'encoding' - supported values are LinearEncoding and sRGBEncoding. Defaulting to LinearEncoding."); value = LinearEncoding; } return value; } _webglContextRestored() { this._state.texture = new Texture2D({gl: this.scene.canvas.gl}); if (this._image) { this.image = this._image; } else if (this._src) { this.src = this._src; } } _update() { const state = this._state; if (this._matrixDirty) { let matrix; let t; if (this._translate[0] !== 0 || this._translate[1] !== 0) { matrix = math.translationMat4v([this._translate[0], this._translate[1], 0], this._state.matrix); } if (this._scale[0] !== 1 || this._scale[1] !== 1) { t = math.scalingMat4v([this._scale[0], this._scale[1], 1]); matrix = matrix ? math.mulMat4(matrix, t) : t; } if (this._rotate !== 0) { t = math.rotationMat4v(this._rotate * 0.0174532925, [0, 0, 1]); matrix = matrix ? math.mulMat4(matrix, t) : t; } if (matrix) { state.matrix = matrix; } this._matrixDirty = false; } this.glRedraw(); } /** * Sets an HTML DOM Image object to source this Texture from. * * Sets {@link Texture#src} null. * * @type {HTMLImageElement} */ set image(value) { this._image = ensureImageSizePowerOfTwo$1(value); this._image.crossOrigin = "Anonymous"; this._state.texture.setImage(this._image, this._state); this._src = null; this.glRedraw(); } /** * Gets HTML DOM Image object this Texture is sourced from, if any. * * Returns null if not set. * * @type {HTMLImageElement} */ get image() { return this._image; } /** * Sets path to an image file to source this Texture from. * * Sets {@link Texture#image} null. * * @type {String} */ set src(src) { this.scene.loading++; this.scene.canvas.spinner.processes++; const self = this; let image = new Image(); image.onload = function () { image = ensureImageSizePowerOfTwo$1(image); self._state.texture.setImage(image, self._state); self.scene.loading--; self.glRedraw(); self.scene.canvas.spinner.processes--; }; image.src = src; this._src = src; this._image = null; } /** * Gets path to the image file this Texture from, if any. * * Returns null if not set. * * @type {String} */ get src() { return this._src; } /** * Sets the 2D translation vector added to this Texture's *S* and *T* UV coordinates. * * Default value is ````[0, 0]````. * * @type {Number[]} */ set translate(value) { this._translate.set(value || [0, 0]); this._matrixDirty = true; this._needUpdate(); } /** * Gets the 2D translation vector added to this Texture's *S* and *T* UV coordinates. * * Default value is ````[0, 0]````. * * @type {Number[]} */ get translate() { return this._translate; } /** * Sets the 2D scaling vector that will be applied to this Texture's *S* and *T* UV coordinates. * * Default value is ````[1, 1]````. * * @type {Number[]} */ set scale(value) { this._scale.set(value || [1, 1]); this._matrixDirty = true; this._needUpdate(); } /** * Gets the 2D scaling vector that will be applied to this Texture's *S* and *T* UV coordinates. * * Default value is ````[1, 1]````. * * @type {Number[]} */ get scale() { return this._scale; } /** * Sets the rotation angles, in degrees, that will be applied to this Texture's *S* and *T* UV coordinates. * * Default value is ````0````. * * @type {Number} */ set rotate(value) { value = value || 0; if (this._rotate === value) { return; } this._rotate = value; this._matrixDirty = true; this._needUpdate(); } /** * Gets the rotation angles, in degrees, that will be applied to this Texture's *S* and *T* UV coordinates. * * Default value is ````0````. * * @type {Number} */ get rotate() { return this._rotate; } /** * Gets how this Texture is sampled when a texel covers less than one pixel. * * Options are: * * * NearestFilter - Uses the value of the texture element that is nearest * (in Manhattan distance) to the center of the pixel being textured. * * * LinearFilter - Uses the weighted average of the four texture elements that are * closest to the center of the pixel being textured. * * * NearestMipMapNearestFilter - Chooses the mipmap that most closely matches the * size of the pixel being textured and uses the "nearest" criterion (the texture * element nearest to the center of the pixel) to produce a texture value. * * * LinearMipMapNearestFilter - Chooses the mipmap that most closely matches the size of * the pixel being textured and uses the "linear" criterion (a weighted average of the * four texture elements that are closest to the center of the pixel) to produce a * texture value. * * * NearestMipMapLinearFilter - Chooses the two mipmaps that most closely * match the size of the pixel being textured and uses the "nearest" criterion * (the texture element nearest to the center of the pixel) to produce a texture * value from each mipmap. The final texture value is a weighted average of those two * values. * * * LinearMipMapLinearFilter - (default) - Chooses the two mipmaps that most closely match the size * of the pixel being textured and uses the "linear" criterion (a weighted average * of the four texture elements that are closest to the center of the pixel) to * produce a texture value from each mipmap. The final texture value is a weighted * average of those two values. * * Default value is LinearMipMapLinearFilter. * * @type {Number} */ get minFilter() { return this._state.minFilter; } /** * Gets how this Texture is sampled when a texel covers more than one pixel. * * * NearestFilter - Uses the value of the texture element that is nearest * (in Manhattan distance) to the center of the pixel being textured. * * LinearFilter - (default) - Uses the weighted average of the four texture elements that are * closest to the center of the pixel being textured. * * Default value is LinearMipMapLinearFilter. * * @type {Number} */ get magFilter() { return this._state.magFilter; } /** * Gets the wrap parameter for this Texture's *S* coordinate. * * Values can be: * * * ClampToEdgeWrapping - causes *S* coordinates to be clamped to the size of the texture. * * MirroredRepeatWrapping - causes the *S* coordinate to be set to the fractional part of the texture coordinate * if the integer part of *S* is even; if the integer part of *S* is odd, then the *S* texture coordinate is * set to *1 - frac ⁡ S* , where *frac ⁡ S* represents the fractional part of *S*. * * RepeatWrapping - (default) - causes the integer part of the *S* coordinate to be ignored; xeokit uses only the * fractional part, thereby creating a repeating pattern. * * Default value is RepeatWrapping. * * @type {Number} */ get wrapS() { return this._state.wrapS; } /** * Gets the wrap parameter for this Texture's *T* coordinate. * * Values can be: * * * ClampToEdgeWrapping - causes *S* coordinates to be clamped to the size of the texture. * * MirroredRepeatWrapping - causes the *S* coordinate to be set to the fractional part of the texture coordinate * if the integer part of *S* is even; if the integer part of *S* is odd, then the *S* texture coordinate is * set to *1 - frac ⁡ S* , where *frac ⁡ S* represents the fractional part of *S*. * * RepeatWrapping - (default) - causes the integer part of the *S* coordinate to be ignored; xeokit uses only the * fractional part, thereby creating a repeating pattern. * * Default value is RepeatWrapping. * * @type {Number} */ get wrapT() { return this._state.wrapT; } /** * Gets if this Texture's source data is flipped along its vertical axis. * * @type {Number} */ get flipY() { return this._state.flipY; } /** * Gets the Texture's encoding format. * * @type {Number} */ get encoding() { return this._state.encoding; } /** * Destroys this Texture */ destroy() { super.destroy(); if (this._state.texture) { this._state.texture.destroy(); } this._state.destroy(); stats.memory.textures--; } } const tempVec3$5 = math.vec3(); const tempVec3b$B = math.vec3(); math.vec3(); const zeroVec$1 = math.vec3([0, -1, 0]); const tempQuat = math.vec4([0, 0, 0, 1]); /** * @desc A plane-shaped 3D object containing a bitmap image. * * Use ````ImagePlane```` to embed bitmap images in your scenes. * * As shown in the examples below, ````ImagePlane```` is useful for creating ground planes from satellite maps and embedding 2D plan * view images in cross-section slicing planes. * * # Example 1: Create a ground plane from a satellite image * * In our first example, we'll load the Schependomlaan model, then use * an ````ImagePlane```` to create a ground plane, which will contain * a satellite image sourced from Google Maps. * * * * [](https://xeokit.github.io/xeokit-sdk/examples/scenegraph/#ImagePlane_groundPlane) * * [[Run this example](https://xeokit.github.io/xeokit-sdk/examples/scenegraph/#ImagePlane_groundPlane)] * * ````javascript * import {Viewer, ImagePlane, XKTLoaderPlugin} from "xeokit-sdk.es.js"; * * const viewer = new Viewer({ * canvasId: "myCanvas", * transparent: true * }); * * viewer.camera.eye = [-8.31, 42.21, 54.30]; * viewer.camera.look = [-0.86, 15.40, 14.90]; * viewer.camera.up = [0.10, 0.83, -0.54]; * * const xktLoader = new XKTLoaderPlugin(viewer); * * xktLoader.load({ // Load IFC model * id: "myModel", * src: "./models/xkt/Schependomlaan.xkt", * edges: true, * * rotation: [0, 22, 0], // Rotate, position and scale the model to align it correctly with the ImagePlane * position: [-8, 0, 15], * scale: [1.1, 1.1, 1.1] * }); * * new ImagePlane(viewer.scene, { * src: "./images/schependomlaanSatMap.png", // Google satellite image; accepted file types are PNG and JPEG * visible: true, // Show the ImagePlane * gridVisible: true, // Show the grid - grid is only visible when ImagePlane is also visible * size: 190, // Size of ImagePlane's longest edge * position: [0, -1, 0], // World-space position of ImagePlane's center * rotation: [0, 0, 0], // Euler angles for X, Y and Z * opacity: 1.0, // Fully opaque * collidable: false, // ImagePlane does not contribute to Scene boundary * clippable: true, // ImagePlane can be clipped by SectionPlanes * pickable: true // Allow the ground plane to be picked * }); * ```` *
* * # Example 2: Embed an image in a cross-section plane * * In our second example, we'll load the Schependomlaan model again, then slice it in half with * a {@link SectionPlanesPlugin}, then use an ````ImagePlane```` to embed a 2D plan view image in the slicing plane. * * * * [](https://xeokit.github.io/xeokit-sdk/examples/scenegraph/#ImagePlane_imageInSectionPlane) * * [[Run this example](https://xeokit.github.io/xeokit-sdk/examples/scenegraph/#ImagePlane_imageInSectionPlane)] * * ````javascript * import {Viewer, XKTLoaderPlugin, SectionPlanesPlugin, ImagePlane} from "xeokit-sdk.es.js"; * * const viewer = new Viewer({ * canvasId: "myCanvas", * transparent: true * }); * * viewer.camera.eye = [-9.11, 20.01, 5.13]; * viewer.camera.look = [9.07, 0.77, -9.78]; * viewer.camera.up = [0.47, 0.76, -0.38]; * * const xktLoader = new XKTLoaderPlugin(viewer); * * const sectionPlanes = new SectionPlanesPlugin(viewer, { * overviewVisible: false * }); * * model = xktLoader.load({ * id: "myModel", * src: "./models/xkt/schependomlaan/schependomlaan.xkt", * metaModelSrc: "./metaModels/schependomlaan/metaModel.json", * edges: true, * }); * * const sectionPlane = sectionPlanes.createSectionPlane({ * id: "mySectionPlane", * pos: [10.95, 1.95, -10.35], * dir: [0.0, -1.0, 0.0] * }); * * const imagePlane = new ImagePlane(viewer.scene, { * src: "./images/schependomlaanPlanView.png", // Plan view image; accepted file types are PNG and JPEG * visible: true, * gridVisible: true, * size: 23.95, * position: sectionPlane.pos, * dir: sectionPlane.dir, * collidable: false, * opacity: 0.75, * clippable: false, // Don't allow ImagePlane to be clipped by the SectionPlane * pickable: false // Don't allow ImagePlane to be picked * }); * ```` */ class ImagePlane extends Component { /** * @constructor * @param {Component} [owner] Owner component. When destroyed, the owner will destroy this ````ImagePlane```` as well. * @param {*} [cfg] ````ImagePlane```` configuration * @param {String} [cfg.id] Optional ID, unique among all components in the parent {@link Scene}, generated automatically when omitted. * @param {Boolean} [cfg.visible=true] Indicates whether or not this ````ImagePlane```` is visible. * @param {Boolean} [cfg.gridVisible=true] Indicates whether or not the grid is visible. Grid is only visible when ````ImagePlane```` is also visible. * @param {Number[]} [cfg.position=[0,0,0]] World-space position of the ````ImagePlane````. * @param {Number[]} [cfg.size=1] World-space size of the longest edge of the ````ImagePlane````. Note that * ````ImagePlane```` sets its aspect ratio to match its image. If we set a value of ````1000````, and the image * has size ````400x300````, then the ````ImagePlane```` will then have size ````1000 x 750````. * @param {Number[]} [cfg.rotation=[0,0,0]] Local rotation of the ````ImagePlane````, as Euler angles given in degrees, for each of the X, Y and Z axis. * @param {Number[]} [cfg.matrix=[1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1]] Modelling transform matrix for the ````ImagePlane````. Overrides the ````position````, ````size```, ````rotation```` and ````dir```` parameters. * @param {Boolean} [cfg.collidable=true] Indicates if the ````ImagePlane```` is initially included in boundary calculations. * @param {Boolean} [cfg.clippable=true] Indicates if the ````ImagePlane```` is initially clippable. * @param {Boolean} [cfg.pickable=true] Indicates if the ````ImagePlane```` is initially pickable. * @param {Number} [cfg.opacity=1.0] ````ImagePlane````'s initial opacity factor, multiplies by the rendered fragment alpha. * @param {String} [cfg.src] URL of image. Accepted file types are PNG and JPEG. * @param {HTMLImageElement} [cfg.image] An ````HTMLImageElement```` to source the image from. Overrides ````src````. */ constructor(owner, cfg = {}) { super(owner, cfg); this._src = null; this._image = null; this._pos = math.vec3(); this._origin = math.vec3(); this._rtcPos = math.vec3(); this._dir = math.vec3(); this._size = 1.0; this._imageSize = math.vec2(); this._texture = new Texture(this); this._plane = new Mesh(this, { geometry: new ReadableGeometry(this, buildPlaneGeometry({ center: [0, 0, 0], xSize: 1, zSize: 1, xSegments: 10, zSegments: 10 })), material: new PhongMaterial(this, { diffuse: [0, 0, 0], ambient: [0, 0, 0], specular: [0, 0, 0], diffuseMap: this._texture, emissiveMap: this._texture, backfaces: true }), clippable: cfg.clippable }); this._grid = new Mesh(this, { geometry: new ReadableGeometry(this, buildGridGeometry({ size: 1, divisions: 10 })), material: new PhongMaterial(this, { diffuse: [0.0, 0.0, 0.0], ambient: [0.0, 0.0, 0.0], emissive: [0.2, 0.8, 0.2] }), position: [0, 0.001, 0.0], clippable: cfg.clippable }); this._node = new Node$1(this, { rotation: [0, 0, 0], position: [0, 0, 0], scale: [1, 1, 1], clippable: false, children: [ this._plane, this._grid ] }); this._gridVisible = false; this.visible = true; this.gridVisible = cfg.gridVisible; this.position = cfg.position; this.rotation = cfg.rotation; this.dir = cfg.dir; this.size = cfg.size; this.collidable = cfg.collidable; this.clippable = cfg.clippable; this.pickable = cfg.pickable; this.opacity = cfg.opacity; if (cfg.image) { this.image = cfg.image; } else { this.src = cfg.src; } } /** * Sets if this ````ImagePlane```` is visible or not. * * Default value is ````true````. * * @param {Boolean} visible Set ````true```` to make this ````ImagePlane```` visible. */ set visible(visible) { this._plane.visible = visible; this._grid.visible = (this._gridVisible && visible); } /** * Gets if this ````ImagePlane```` is visible or not. * * Default value is ````true````. * * @returns {Boolean} Returns ````true```` if visible. */ get visible() { return this._plane.visible; } /** * Sets if this ````ImagePlane````'s grid is visible or not. * * Default value is ````false````. * * Grid is only visible when ````ImagePlane```` is also visible. * * @param {Boolean} visible Set ````true```` to make this ````ImagePlane````'s grid visible. */ set gridVisible(visible) { visible = (visible !== false); this._gridVisible = visible; this._grid.visible = (this._gridVisible && this.visible); } /** * Gets if this ````ImagePlane````'s grid is visible or not. * * Default value is ````false````. * * @returns {Boolean} Returns ````true```` if visible. */ get gridVisible() { return this._gridVisible; } /** * Sets an ````HTMLImageElement```` to source the image from. * * Sets {@link Texture#src} null. * * @type {HTMLImageElement} */ set image(image) { this._image = image; if (this._image) { this._imageSize[0] = image.width; this._imageSize[1] = image.height; this._updatePlaneSizeFromImage(); this._src = null; this._texture.image = this._image; } } /** * Gets the ````HTMLImageElement```` the ````ImagePlane````'s image is sourced from, if set. * * Returns null if not set. * * @type {HTMLImageElement} */ get image() { return this._image; } /** * Sets an image file path that the ````ImagePlane````'s image is sourced from. * * Accepted file types are PNG and JPEG. * * Sets {@link Texture#image} null. * * @type {String} */ set src(src) { this._src = src; if (this._src) { this._image = null; const image = new Image(); image.onload = () => { this._texture.image = image; this._imageSize[0] = image.width; this._imageSize[1] = image.height; this._updatePlaneSizeFromImage(); }; image.src = this._src; } } /** * Gets the image file path that the ````ImagePlane````'s image is sourced from, if set. * * Returns null if not set. * * @type {String} */ get src() { return this._src; } /** * Sets the World-space position of this ````ImagePlane````. * * Default value is ````[0, 0, 0]````. * * @param {Number[]} value New position. */ set position(value) { this._pos.set(value || [0, 0, 0]); worldToRTCPos(this._pos, this._origin, this._rtcPos); this._node.origin = this._origin; this._node.position = this._rtcPos; } /** * Gets the World-space position of this ````ImagePlane````. * * Default value is ````[0, 0, 0]````. * * @returns {Number[]} Current position. */ get position() { return this._pos; } /** * Sets the direction of this ````ImagePlane```` using Euler angles. * * Default value is ````[0, 0, 0]````. * * @param {Number[]} value Euler angles for ````X````, ````Y```` and ````Z```` axis rotations. */ set rotation(value) { this._node.rotation = value; } /** * Gets the direction of this ````ImagePlane```` as Euler angles. * * @returns {Number[]} Euler angles for ````X````, ````Y```` and ````Z```` axis rotations. */ get rotation() { return this._node.rotation; } /** * Sets the World-space size of the longest edge of the ````ImagePlane````. * * Note that ````ImagePlane```` sets its aspect ratio to match its image. If we set a value of ````1000````, and * the image has size ````400x300````, then the ````ImagePlane```` will then have size ````1000 x 750````. * * Default value is ````1.0````. * * @param {Number} size New World-space size of the ````ImagePlane````. */ set size(size) { this._size = (size === undefined || size === null) ? 1.0 : size; if (this._image) { this._updatePlaneSizeFromImage(); } } /** * Gets the World-space size of the longest edge of the ````ImagePlane````. * * Returns {Number} World-space size of the ````ImagePlane````. */ get size() { return this._size; } /** * Sets the direction of this ````ImagePlane```` as a direction vector. * * Default value is ````[0, 0, -1]````. * * @param {Number[]} dir New direction vector. */ set dir(dir) { this._dir.set(dir || [0, 0, -1]); if (dir) { const origin = this.scene.center; const negDir = [-this._dir[0], -this._dir[1], -this._dir[2]]; math.subVec3(origin, this.position, tempVec3$5); const dist = -math.dotVec3(negDir, tempVec3$5); math.normalizeVec3(negDir); math.mulVec3Scalar(negDir, dist, tempVec3b$B); math.vec3PairToQuaternion(zeroVec$1, dir, tempQuat); this._node.quaternion = tempQuat; } } /** * Gets the direction of this ````ImagePlane```` as a direction vector. * * @returns {Number[]} value Current direction. */ get dir() { return this._dir; } /** * Sets if this ````ImagePlane```` is included in boundary calculations. * * Default is ````true````. * * @type {Boolean} */ set collidable(value) { this._node.collidable = (value !== false); } /** * Gets if this ````ImagePlane```` is included in boundary calculations. * * Default is ````true````. * * @type {Boolean} */ get collidable() { return this._node.collidable; } /** * Sets if this ````ImagePlane```` is clippable. * * Clipping is done by the {@link SectionPlane}s in {@link Scene#sectionPlanes}. * * Default is ````true````. * * @type {Boolean} */ set clippable(value) { this._node.clippable = (value !== false); } /** * Gets if this ````ImagePlane```` is clippable. * * Clipping is done by the {@link SectionPlane}s in {@link Scene#sectionPlanes}. * * Default is ````true````. * * @type {Boolean} */ get clippable() { return this._node.clippable; } /** * Sets if this ````ImagePlane```` is pickable. * * Default is ````true````. * * @type {Boolean} */ set pickable(value) { this._node.pickable = (value !== false); } /** * Gets if this ````ImagePlane```` is pickable. * * Default is ````true````. * * @type {Boolean} */ get pickable() { return this._node.pickable; } /** * Sets the opacity factor for this ````ImagePlane````. * * This is a factor in range ````[0..1]```` which multiplies by the rendered fragment alphas. * * @type {Number} */ set opacity(opacity) { this._node.opacity = opacity; } /** * Gets this ````ImagePlane````'s opacity factor. * * This is a factor in range ````[0..1]```` which multiplies by the rendered fragment alphas. * * @type {Number} */ get opacity() { return this._node.opacity; } /** * @destroy */ destroy() { super.destroy(); } _updatePlaneSizeFromImage() { const size = this._size; const width = this._imageSize[0]; const height = this._imageSize[1]; if (width > height) { const aspect = height / width; this._node.scale = [size, 1.0, size * aspect]; } else { const aspect = width / height; this._node.scale = [size * aspect, 1.0, size]; } } } /** * @desc Configures the normal rendered appearance of {@link Mesh}es using the non-realistic but GPU-efficient Lambertian flat shading model for calculating reflectance. * * * Useful for efficiently rendering non-realistic objects for high-detail CAD. * * Use {@link PhongMaterial} when you need specular highlights. * * Use the physically-based {@link MetallicMaterial} or {@link SpecularMaterial} when you need more realism. * * For LambertMaterial, the illumination calculation is performed at each triangle vertex, and the resulting color is interpolated across the face of the triangle. For {@link PhongMaterial}, {@link MetallicMaterial} and * {@link SpecularMaterial}, vertex normals are interpolated across the surface of the triangle, and the illumination calculation is performed at each texel. * * ## Usage * * [[Run this example](/examples/index.html#materials_LambertMaterial)] * * In the example below we'll create a {@link Mesh} with a shape defined by a {@link buildTorusGeometry} and normal rendering appearance configured with a LambertMaterial. * * ```` javascript * import {Viewer, Mesh, buildTorusGeometry, ReadableGeometry, LambertMaterial} from "xeokit-sdk.es.js"; * * const viewer = new Viewer({ * canvasId: "myCanvas" * }); * * viewer.scene.camera.eye = [0, 0, 5]; * viewer.scene.camera.look = [0, 0, 0]; * viewer.scene.camera.up = [0, 1, 0]; * * new Mesh(viewer.scene, { * geometry: new ReadableGeometry(viewer.scene, buildTorusGeometry({ * center: [0, 0, 0], * radius: 1.5, * tube: 0.5, * radialSegments: 12, * tubeSegments: 8, * arc: Math.PI * 2.0 * }), * material: new LambertMaterial(viewer.scene, { * ambient: [0.3, 0.3, 0.3], * color: [0.5, 0.5, 0.0], * alpha: 1.0, // Default * lineWidth: 1, * pointSize: 1, * backfaces: false, * frontFace: "ccw" * }) * }); * ```` * * ## LambertMaterial Properties * * The following table summarizes LambertMaterial properties: * * | Property | Type | Range | Default Value | Space | Description | * |:--------:|:----:|:-----:|:-------------:|:-----:|:-----------:| * | {@link LambertMaterial#ambient} | Array | [0, 1] for all components | [1,1,1,1] | linear | The RGB components of the ambient light reflected by the material. | * | {@link LambertMaterial#color} | Array | [0, 1] for all components | [1,1,1,1] | linear | The RGB components of the diffuse light reflected by the material. | * | {@link LambertMaterial#emissive} | Array | [0, 1] for all components | [0,0,0] | linear | The RGB components of the light emitted by the material. | * | {@link LambertMaterial#alpha} | Number | [0, 1] | 1 | linear | The transparency of the material surface (0 fully transparent, 1 fully opaque). | * | {@link LambertMaterial#lineWidth} | Number | [0..100] | 1 | | Line width in pixels. | * | {@link LambertMaterial#pointSize} | Number | [0..100] | 1 | | Point size in pixels. | * | {@link LambertMaterial#backfaces} | Boolean | | false | | Whether to render {@link Geometry} backfaces. | * | {@link LambertMaterial#frontface} | String | "ccw", "cw" | "ccw" | | The winding order for {@link Geometry} frontfaces - "cw" for clockwise, or "ccw" for counter-clockwise. | * */ class LambertMaterial extends Material { /** @private */ get type() { return "LambertMaterial"; } /** * @constructor * @param {Component} owner Owner component. When destroyed, the owner will destroy this component as well. * @param {*} [cfg] The LambertMaterial configuration * @param {String} [cfg.id] Optional ID, unique among all components in the parent {@link Scene}, generated automatically when omitted. * @param {String:Object} [cfg.meta=null] Metadata to attach to this LambertMaterial. * @param {Number[]} [cfg.ambient=[1.0, 1.0, 1.0 ]] LambertMaterial ambient color. * @param {Number[]} [cfg.color=[ 1.0, 1.0, 1.0 ]] LambertMaterial diffuse color. * @param {Number[]} [cfg.emissive=[ 0.0, 0.0, 0.0 ]] LambertMaterial emissive color. * @param {Number} [cfg.alpha=1]Scalar in range 0-1 that controls alpha, where 0 is completely transparent and 1 is completely opaque. * @param {Number} [cfg.reflectivity=1]Scalar in range 0-1 that controls how much {@link ReflectionMap} is reflected. * @param {Number} [cfg.lineWidth=1] Scalar that controls the width of {@link Geometry} lines. * @param {Number} [cfg.pointSize=1] Scalar that controls the size of points for {@link Geometry} with {@link Geometry#primitive} set to "points". * @param {Boolean} [cfg.backfaces=false] Whether to render {@link Geometry} backfaces. * @param {Boolean} [cfg.frontface="ccw"] The winding order for {@link Geometry} front faces - "cw" for clockwise, or "ccw" for counter-clockwise. */ constructor(owner, cfg = {}) { super(owner, cfg); this._state = new RenderState({ type: "LambertMaterial", ambient: math.vec3([1.0, 1.0, 1.0]), color: math.vec3([1.0, 1.0, 1.0]), emissive: math.vec3([0.0, 0.0, 0.0]), alpha: null, alphaMode: 0, // 2 ("blend") when transparent, so renderer knows when to add to transparency bin lineWidth: null, pointSize: null, backfaces: null, frontface: null, // Boolean for speed; true == "ccw", false == "cw" hash: "/lam;" }); this.ambient = cfg.ambient; this.color = cfg.color; this.emissive = cfg.emissive; this.alpha = cfg.alpha; this.lineWidth = cfg.lineWidth; this.pointSize = cfg.pointSize; this.backfaces = cfg.backfaces; this.frontface = cfg.frontface; } /** * Sets the LambertMaterial's ambient color. * * Default value is ````[0.3, 0.3, 0.3]````. * * @type {Number[]} */ set ambient(value) { let ambient = this._state.ambient; if (!ambient) { ambient = this._state.ambient = new Float32Array(3); } else if (value && ambient[0] === value[0] && ambient[1] === value[1] && ambient[2] === value[2]) { return; } if (value) { ambient[0] = value[0]; ambient[1] = value[1]; ambient[2] = value[2]; } else { ambient[0] = .2; ambient[1] = .2; ambient[2] = .2; } this.glRedraw(); } /** * Gets the LambertMaterial's ambient color. * * Default value is ````[0.3, 0.3, 0.3]````. * * @type {Number[]} */ get ambient() { return this._state.ambient; } /** * Sets the LambertMaterial's diffuse color. * * Default value is ````[1.0, 1.0, 1.0]````. * * @type {Number[]} */ set color(value) { let color = this._state.color; if (!color) { color = this._state.color = new Float32Array(3); } else if (value && color[0] === value[0] && color[1] === value[1] && color[2] === value[2]) { return; } if (value) { color[0] = value[0]; color[1] = value[1]; color[2] = value[2]; } else { color[0] = 1; color[1] = 1; color[2] = 1; } this.glRedraw(); } /** * Gets the LambertMaterial's diffuse color. * * Default value is ````[1.0, 1.0, 1.0]````. * * @type {Number[]} */ get color() { return this._state.color; } /** * Sets the LambertMaterial's emissive color. * * Default value is ````[0.0, 0.0, 0.0]````. * * @type {Number[]} */ set emissive(value) { let emissive = this._state.emissive; if (!emissive) { emissive = this._state.emissive = new Float32Array(3); } else if (value && emissive[0] === value[0] && emissive[1] === value[1] && emissive[2] === value[2]) { return; } if (value) { emissive[0] = value[0]; emissive[1] = value[1]; emissive[2] = value[2]; } else { emissive[0] = 0; emissive[1] = 0; emissive[2] = 0; } this.glRedraw(); } /** * Gets the LambertMaterial's emissive color. * * Default value is ````[0.0, 0.0, 0.0]````. * * @type {Number[]} */ get emissive() { return this._state.emissive; } /** * Sets factor in the range ````[0..1]```` indicating how transparent the LambertMaterial is. * * A value of ````0.0```` indicates fully transparent, ````1.0```` is fully opaque. * * Default value is ````1.0```` * * @type {Number} */ set alpha(value) { value = (value !== undefined && value !== null) ? value : 1.0; if (this._state.alpha === value) { return; } this._state.alpha = value; this._state.alphaMode = value < 1.0 ? 2 /* blend */ : 0; /* opaque */ this.glRedraw(); } /** * Gets factor in the range ````[0..1]```` indicating how transparent the LambertMaterial is. * * A value of ````0.0```` indicates fully transparent, ````1.0```` is fully opaque. * * Default value is ````1.0```` * * @type {Number} */ get alpha() { return this._state.alpha; } /** * Sets the LambertMaterial's line width. * * This is not supported by WebGL implementations based on DirectX [2019]. * * Default value is ````1.0````. * * @type {Number} */ set lineWidth(value) { this._state.lineWidth = value || 1.0; this.glRedraw(); } /** * Gets the LambertMaterial's line width. * * This is not supported by WebGL implementations based on DirectX [2019]. * * Default value is ````1.0````. * * @type {Number} */ get lineWidth() { return this._state.lineWidth; } /** * Sets the LambertMaterial's point size. * * Default value is ````1.0````. * * @type {Number} */ set pointSize(value) { this._state.pointSize = value || 1.0; this.glRedraw(); } /** * Gets the LambertMaterial's point size. * * Default value is ````1.0````. * * @type {Number} */ get pointSize() { return this._state.pointSize; } /** * Sets whether backfaces are visible on attached {@link Mesh}es. * * @type {Boolean} */ set backfaces(value) { value = !!value; if (this._state.backfaces === value) { return; } this._state.backfaces = value; this.glRedraw(); } /** * Gets whether backfaces are visible on attached {@link Mesh}es. * * @type {Boolean} */ get backfaces() { return this._state.backfaces; } /** * Sets the winding direction of front faces of {@link Geometry} of attached {@link Mesh}es. * * Default value is ````"ccw"````. * * @type {String} */ set frontface(value) { value = value !== "cw"; if (this._state.frontface === value) { return; } this._state.frontface = value; this.glRedraw(); } /** * Gets the winding direction of front faces of {@link Geometry} of attached {@link Mesh}es. * * Default value is ````"ccw"````. * * @type {String} */ get frontface() { return this._state.frontface ? "ccw" : "cw"; } _getState() { return this._state; } /** * Destroys this LambertMaterial. */ destroy() { super.destroy(); this._state.destroy(); } } const modes = {"opaque": 0, "mask": 1, "blend": 2}; const modeNames = ["opaque", "mask", "blend"]; /** * @desc Configures the normal rendered appearance of {@link Mesh}es using the physically-accurate *metallic-roughness* shading model. * * * Useful for conductive materials, such as metal, but also appropriate for insulators. * * {@link SpecularMaterial} is best for insulators, such as wood, ceramics and plastic. * * {@link PhongMaterial} is appropriate for non-realistic objects. * * {@link LambertMaterial} is appropriate for high-detail models that need to render as efficiently as possible. * * ## Usage * * In the example below we'll create a {@link Mesh} with {@link MetallicMaterial} and {@link ReadableGeometry} loaded from OBJ. * * Note that in this example we're providing separate {@link Texture} for the {@link MetallicMaterial#metallic} and {@link MetallicMaterial#roughness} * channels, which allows us a little creative flexibility. Then, in the next example further down, we'll combine those channels * within the same {@link Texture} for efficiency. * * [[Run this example](/examples/index.html#materials_MetallicMaterial)] * * ````javascript * import {Viewer, Mesh, loadOBJGeometry, ReadableGeometry, MetallicMaterial, Texture} from "xeokit-sdk.es.js"; * * const viewer = new Viewer({ * canvasId: "myCanvas" * }); * * viewer.scene.camera.eye = [0.57, 1.37, 1.14]; * viewer.scene.camera.look = [0.04, 0.58, 0.00]; * viewer.scene.camera.up = [-0.22, 0.84, -0.48]; * * loadOBJGeometry(viewer.scene, { * src: "models/obj/fireHydrant/FireHydrantMesh.obj" * }) * .then(function (geometry) { * * // Success * * new Mesh(viewer.scene, { * * geometry: new ReadableGeometry(viewer.scene, geometry), * * material: new MetallicMaterial(viewer.scene, { * * baseColor: [1, 1, 1], * metallic: 1.0, * roughness: 1.0, * * baseColorMap: new Texture(viewer.scene, { * src: "models/obj/fireHydrant/fire_hydrant_Base_Color.png", * encoding: "sRGB" * }), * normalMap: new Texture(viewer.scene, { * src: "models/obj/fireHydrant/fire_hydrant_Normal_OpenGL.png" * }), * roughnessMap: new Texture(viewer.scene, { * src: "models/obj/fireHydrant/fire_hydrant_Roughness.png" * }), * metallicMap: new Texture(viewer.scene, { * src: "models/obj/fireHydrant/fire_hydrant_Metallic.png" * }), * occlusionMap: new Texture(viewer.scene, { * src: "models/obj/fireHydrant/fire_hydrant_Mixed_AO.png" * }), * * specularF0: 0.7 * }) * }); * }, function () { * // Error * }); * ```` * * ## Background Theory * * For an introduction to physically-based rendering (PBR) concepts, try these articles: * * * Joe Wilson's [Basic Theory of Physically-Based Rendering](https://www.marmoset.co/posts/basic-theory-of-physically-based-rendering/) * * Jeff Russel's [Physically-based Rendering, and you can too!](https://www.marmoset.co/posts/physically-based-rendering-and-you-can-too/) * * Sebastien Legarde's [Adapting a physically-based shading model](http://seblagarde.wordpress.com/tag/physically-based-rendering/) * * ## MetallicMaterial Properties * * The following table summarizes MetallicMaterial properties: * * | Property | Type | Range | Default Value | Space | Description | * |:--------:|:----:|:-----:|:-------------:|:-----:|:-----------:| * | {@link MetallicMaterial#baseColor} | Array | [0, 1] for all components | [1,1,1,1] | linear | The RGB components of the base color of the material. | * | {@link MetallicMaterial#metallic} | Number | [0, 1] | 1 | linear | The metallic-ness the material (1 for metals, 0 for non-metals). | * | {@link MetallicMaterial#roughness} | Number | [0, 1] | 1 | linear | The roughness of the material surface. | * | {@link MetallicMaterial#specularF0} | Number | [0, 1] | 1 | linear | The specular Fresnel of the material surface. | * | {@link MetallicMaterial#emissive} | Array | [0, 1] for all components | [0,0,0] | linear | The RGB components of the emissive color of the material. | * | {@link MetallicMaterial#alpha} | Number | [0, 1] | 1 | linear | The transparency of the material surface (0 fully transparent, 1 fully opaque). | * | {@link MetallicMaterial#baseColorMap} | {@link Texture} | | null | sRGB | Texture RGB components multiplying by {@link MetallicMaterial#baseColor}. If the fourth component (A) is present, it multiplies by {@link MetallicMaterial#alpha}. | * | {@link MetallicMaterial#metallicMap} | {@link Texture} | | null | linear | Texture with first component multiplying by {@link MetallicMaterial#metallic}. | * | {@link MetallicMaterial#roughnessMap} | {@link Texture} | | null | linear | Texture with first component multiplying by {@link MetallicMaterial#roughness}. | * | {@link MetallicMaterial#metallicRoughnessMap} | {@link Texture} | | null | linear | Texture with first component multiplying by {@link MetallicMaterial#metallic} and second component multiplying by {@link MetallicMaterial#roughness}. | * | {@link MetallicMaterial#emissiveMap} | {@link Texture} | | null | linear | Texture with RGB components multiplying by {@link MetallicMaterial#emissive}. | * | {@link MetallicMaterial#alphaMap} | {@link Texture} | | null | linear | Texture with first component multiplying by {@link MetallicMaterial#alpha}. | * | {@link MetallicMaterial#occlusionMap} | {@link Texture} | | null | linear | Ambient occlusion texture multiplying by surface's reflected diffuse and specular light. | * | {@link MetallicMaterial#normalMap} | {@link Texture} | | null | linear | Tangent-space normal map. | * | {@link MetallicMaterial#alphaMode} | String | "opaque", "blend", "mask" | "blend" | | Alpha blend mode. | * | {@link MetallicMaterial#alphaCutoff} | Number | [0..1] | 0.5 | | Alpha cutoff value. | * | {@link MetallicMaterial#backfaces} | Boolean | | false | | Whether to render {@link ReadableGeometry} backfaces. | * | {@link MetallicMaterial#frontface} | String | "ccw", "cw" | "ccw" | | The winding order for {@link ReadableGeometry} frontfaces - "cw" for clockwise, or "ccw" for counter-clockwise. | * * * ## Combining Channels Within the Same Textures * * In the previous example we provided separate {@link Texture} for the {@link MetallicMaterial#metallic} and * {@link MetallicMaterial#roughness} channels, but we can combine those channels into the same {@link Texture} to * reduce download time, memory footprint and rendering time (and also for glTF compatibility). * * Here's the {@link Mesh} again, with our MetallicMaterial with those channels combined in the {@link MetallicMaterial#metallicRoughnessMap} * {@link Texture}, where the *R* component multiplies by {@link MetallicMaterial#metallic} and *G* multiplies * by {@link MetallicMaterial#roughness}. * * ````javascript * new Mesh(viewer.scene, { * * geometry: geometry, * * material: new MetallicMaterial(viewer.scene, { * * baseColor: [1, 1, 1], * metallic: 1.0, * roughness: 1.0, * * baseColorMap: new Texture(viewer.scene, { * src: "models/obj/fireHydrant/fire_hydrant_Base_Color.png", * encoding: "sRGB" * }), * normalMap: new Texture(viewer.scene, { * src: "models/obj/fireHydrant/fire_hydrant_Normal_OpenGL.png" * }), * metallicRoughnessMap: new Texture(viewer.scene, { * src: "models/obj/fireHydrant/fire_hydrant_MetallicRoughness.png" * }), * metallicRoughnessMap : new Texture(viewer.scene, { // <<----------- Added * src: "models/obj/fireHydrant/fire_hydrant_MetallicRoughness.png" // R component multiplies by metallic * }), // G component multiplies by roughness * occlusionMap: new Texture(viewer.scene, { * src: "models/obj/fireHydrant/fire_hydrant_Mixed_AO.png" * }), * * specularF0: 0.7 * }) * ```` * * Although not shown in this example, we can also texture {@link MetallicMaterial#alpha} with the *A* component of * {@link MetallicMaterial#baseColorMap}'s {@link Texture}, if required. * * ## Alpha Blending * * Let's make our {@link Mesh} transparent. * * We'll update the {@link MetallicMaterial#alpha} and {@link MetallicMaterial#alphaMode}, causing it to blend 50% * with the background: * * ````javascript * hydrant.material.alpha = 0.5; * hydrant.material.alphaMode = "blend"; * ```` * * ## Alpha Masking * * Let's apply an alpha mask to our {@link Mesh}. * * We'll configure an {@link MetallicMaterial#alphaMap} to multiply by {@link MetallicMaterial#alpha}, * with {@link MetallicMaterial#alphaMode} and {@link MetallicMaterial#alphaCutoff} to treat it as an alpha mask: * * ````javascript * new Mesh(viewer.scene, { * * geometry: geometry, * * material: new MetallicMaterial(viewer.scene, { * * baseColor: [1, 1, 1], * metallic: 1.0, * roughness: 1.0, * alpha: 1.0, * alphaMode : "mask", // <<---------------- Added * alphaCutoff : 0.2, // <<---------------- Added * * alphaMap : new Texture(viewer.scene{ // <<---------------- Added * src: "textures/alphaMap.jpg" * }), * baseColorMap: new Texture(viewer.scene, { * src: "models/obj/fireHydrant/fire_hydrant_Base_Color.png", * encoding: "sRGB" * }), * normalMap: new Texture(viewer.scene, { * src: "models/obj/fireHydrant/fire_hydrant_Normal_OpenGL.png" * }), * metallicRoughnessMap: new Texture(viewer.scene, { * src: "models/obj/fireHydrant/fire_hydrant_MetallicRoughness.png" * }), * metallicRoughnessMap : new Texture(viewer.scene, { // <<----------- Added * src: "models/obj/fireHydrant/fire_hydrant_MetallicRoughness.png" // R component multiplies by metallic * }), // G component multiplies by roughness * occlusionMap: new Texture(viewer.scene, { * src: "models/obj/fireHydrant/fire_hydrant_Mixed_AO.png" * }), * * specularF0: 0.7 * }) * ```` */ class MetallicMaterial extends Material { /** @private */ get type() { return "MetallicMaterial"; } /** * @constructor * @param {Component} owner Owner component. When destroyed, the owner will destroy this MetallicMaterial as well. * @param {*} [cfg] The MetallicMaterial configuration. * @param {String} [cfg.id] Optional ID, unique among all components in the parent {@link Scene}, generated automatically when omitted. * @param {Number[]} [cfg.baseColor=[1,1,1]] RGB diffuse color of this MetallicMaterial. Multiplies by the RGB components of {@link MetallicMaterial#baseColorMap}. * @param {Number} [cfg.metallic=1.0] Factor in the range ````[0..1]```` indicating how metallic this MetallicMaterial is. ````1```` is metal, ````0```` is non-metal. Multiplies by the *R* component of {@link MetallicMaterial#metallicMap} and the *A* component of {@link MetallicMaterial#metallicRoughnessMap}. * @param {Number} [cfg.roughness=1.0] Factor in the range ````[0..1]```` indicating the roughness of this MetallicMaterial. ````0```` is fully smooth, ````1```` is fully rough. Multiplies by the *R* component of {@link MetallicMaterial#roughnessMap}. * @param {Number} [cfg.specularF0=0.0] Factor in the range ````[0..1]```` indicating specular Fresnel. * @param {Number[]} [cfg.emissive=[0,0,0]] RGB emissive color of this MetallicMaterial. Multiplies by the RGB components of {@link MetallicMaterial#emissiveMap}. * @param {Number} [cfg.alpha=1.0] Factor in the range ````[0..1]```` indicating the alpha of this MetallicMaterial. Multiplies by the *R* component of {@link MetallicMaterial#alphaMap} and the *A* component, if present, of {@link MetallicMaterial#baseColorMap}. The value of {@link MetallicMaterial#alphaMode} indicates how alpha is interpreted when rendering. * @param {Texture} [cfg.baseColorMap=undefined] RGBA {@link Texture} containing the diffuse color of this MetallicMaterial, with optional *A* component for alpha. The RGB components multiply by the {@link MetallicMaterial#baseColor} property, while the *A* component, if present, multiplies by the {@link MetallicMaterial#alpha} property. * @param {Texture} [cfg.alphaMap=undefined] RGB {@link Texture} containing this MetallicMaterial's alpha in its *R* component. The *R* component multiplies by the {@link MetallicMaterial#alpha} property. Must be within the same {@link Scene} as this MetallicMaterial. * @param {Texture} [cfg.metallicMap=undefined] RGB {@link Texture} containing this MetallicMaterial's metallic factor in its *R* component. The *R* component multiplies by the {@link MetallicMaterial#metallic} property. Must be within the same {@link Scene} as this MetallicMaterial. * @param {Texture} [cfg.roughnessMap=undefined] RGB {@link Texture} containing this MetallicMaterial's roughness factor in its *R* component. The *R* component multiplies by the {@link MetallicMaterial#roughness} property. Must be within the same {@link Scene} as this MetallicMaterial. * @param {Texture} [cfg.metallicRoughnessMap=undefined] RGB {@link Texture} containing this MetallicMaterial's metalness in its *R* component and roughness in its *G* component. Its *R* component multiplies by the {@link MetallicMaterial#metallic} property, while its *G* component multiplies by the {@link MetallicMaterial#roughness} property. Must be within the same {@link Scene} as this MetallicMaterial. * @param {Texture} [cfg.emissiveMap=undefined] RGB {@link Texture} containing the emissive color of this MetallicMaterial. Multiplies by the {@link MetallicMaterial#emissive} property. Must be within the same {@link Scene} as this MetallicMaterial. * @param {Texture} [cfg.occlusionMap=undefined] RGB ambient occlusion {@link Texture}. Within shaders, multiplies by the specular and diffuse light reflected by surfaces. Must be within the same {@link Scene} as this MetallicMaterial. * @param {Texture} [cfg.normalMap=undefined] RGB tangent-space normal {@link Texture}. Must be within the same {@link Scene} as this MetallicMaterial. * @param {String} [cfg.alphaMode="opaque"] The alpha blend mode, which specifies how alpha is to be interpreted. Accepted values are "opaque", "blend" and "mask". See the {@link MetallicMaterial#alphaMode} property for more info. * @param {Number} [cfg.alphaCutoff=0.5] The alpha cutoff value. See the {@link MetallicMaterial#alphaCutoff} property for more info. * @param {Boolean} [cfg.backfaces=false] Whether to render {@link ReadableGeometry} backfaces. * @param {Boolean} [cfg.frontface="ccw"] The winding order for {@link ReadableGeometry} front faces - ````"cw"```` for clockwise, or ````"ccw"```` for counter-clockwise. * @param {Number} [cfg.lineWidth=1] Scalar that controls the width of lines for {@link ReadableGeometry} with {@link ReadableGeometry#primitive} set to "lines". * @param {Number} [cfg.pointSize=1] Scalar that controls the size of points for {@link ReadableGeometry} with {@link ReadableGeometry#primitive} set to "points". */ constructor(owner, cfg = {}) { super(owner, cfg); this._state = new RenderState({ type: "MetallicMaterial", baseColor: math.vec4([1.0, 1.0, 1.0]), emissive: math.vec4([0.0, 0.0, 0.0]), metallic: null, roughness: null, specularF0: null, alpha: null, alphaMode: null, // "opaque" alphaCutoff: null, lineWidth: null, pointSize: null, backfaces: null, frontface: null, // Boolean for speed; true == "ccw", false == "cw" hash: null }); this.baseColor = cfg.baseColor; this.metallic = cfg.metallic; this.roughness = cfg.roughness; this.specularF0 = cfg.specularF0; this.emissive = cfg.emissive; this.alpha = cfg.alpha; if (cfg.baseColorMap) { this._baseColorMap = this._checkComponent("Texture", cfg.baseColorMap); } if (cfg.metallicMap) { this._metallicMap = this._checkComponent("Texture", cfg.metallicMap); } if (cfg.roughnessMap) { this._roughnessMap = this._checkComponent("Texture", cfg.roughnessMap); } if (cfg.metallicRoughnessMap) { this._metallicRoughnessMap = this._checkComponent("Texture", cfg.metallicRoughnessMap); } if (cfg.emissiveMap) { this._emissiveMap = this._checkComponent("Texture", cfg.emissiveMap); } if (cfg.occlusionMap) { this._occlusionMap = this._checkComponent("Texture", cfg.occlusionMap); } if (cfg.alphaMap) { this._alphaMap = this._checkComponent("Texture", cfg.alphaMap); } if (cfg.normalMap) { this._normalMap = this._checkComponent("Texture", cfg.normalMap); } this.alphaMode = cfg.alphaMode; this.alphaCutoff = cfg.alphaCutoff; this.backfaces = cfg.backfaces; this.frontface = cfg.frontface; this.lineWidth = cfg.lineWidth; this.pointSize = cfg.pointSize; this._makeHash(); } _makeHash() { const state = this._state; const hash = ["/met"]; if (this._baseColorMap) { hash.push("/bm"); if (this._baseColorMap._state.hasMatrix) { hash.push("/mat"); } hash.push("/" + this._baseColorMap._state.encoding); } if (this._metallicMap) { hash.push("/mm"); if (this._metallicMap._state.hasMatrix) { hash.push("/mat"); } } if (this._roughnessMap) { hash.push("/rm"); if (this._roughnessMap._state.hasMatrix) { hash.push("/mat"); } } if (this._metallicRoughnessMap) { hash.push("/mrm"); if (this._metallicRoughnessMap._state.hasMatrix) { hash.push("/mat"); } } if (this._emissiveMap) { hash.push("/em"); if (this._emissiveMap._state.hasMatrix) { hash.push("/mat"); } } if (this._occlusionMap) { hash.push("/ocm"); if (this._occlusionMap._state.hasMatrix) { hash.push("/mat"); } } if (this._alphaMap) { hash.push("/am"); if (this._alphaMap._state.hasMatrix) { hash.push("/mat"); } } if (this._normalMap) { hash.push("/nm"); if (this._normalMap._state.hasMatrix) { hash.push("/mat"); } } hash.push(";"); state.hash = hash.join(""); } /** * Sets the RGB diffuse color. * * Multiplies by the RGB components of {@link MetallicMaterial#baseColorMap}. * * Default value is ````[1.0, 1.0, 1.0]````. * @type {Number[]} */ set baseColor(value) { let baseColor = this._state.baseColor; if (!baseColor) { baseColor = this._state.baseColor = new Float32Array(3); } else if (value && baseColor[0] === value[0] && baseColor[1] === value[1] && baseColor[2] === value[2]) { return; } if (value) { baseColor[0] = value[0]; baseColor[1] = value[1]; baseColor[2] = value[2]; } else { baseColor[0] = 1; baseColor[1] = 1; baseColor[2] = 1; } this.glRedraw(); } /** * Gets the RGB diffuse color. * * Multiplies by the RGB components of {@link MetallicMaterial#baseColorMap}. * * Default value is ````[1.0, 1.0, 1.0]````. * @type {Number[]} */ get baseColor() { return this._state.baseColor; } /** * Gets the RGB {@link Texture} containing the diffuse color of this MetallicMaterial, with optional *A* component for alpha. * * The RGB components multiply by {@link MetallicMaterial#baseColor}, while the *A* component, if present, multiplies by {@link MetallicMaterial#alpha}. * * @type {Texture} */ get baseColorMap() { return this._baseColorMap; } /** * Sets the metallic factor. * * This is in the range ````[0..1]```` and indicates how metallic this MetallicMaterial is. * * ````1```` is metal, ````0```` is non-metal. * * Multiplies by the *R* component of {@link MetallicMaterial#metallicMap} and the *A* component of {@link MetallicMaterial#metallicRoughnessMap}. * * Default value is ````1.0````. * * @type {Number} */ set metallic(value) { value = (value !== undefined && value !== null) ? value : 1.0; if (this._state.metallic === value) { return; } this._state.metallic = value; this.glRedraw(); } /** * Gets the metallic factor. * * @type {Number} */ get metallic() { return this._state.metallic; } /** * Gets the RGB {@link Texture} containing this MetallicMaterial's metallic factor in its *R* component. * * The *R* component multiplies by {@link MetallicMaterial#metallic}. * * @type {Texture} */ get metallicMap() { return this._attached.metallicMap; } /** * Sets the roughness factor. * * This factor is in the range ````[0..1]````, where ````0```` is fully smooth,````1```` is fully rough. * * Multiplies by the *R* component of {@link MetallicMaterial#roughnessMap}. * * Default value is ````1.0````. * * @type {Number} */ set roughness(value) { value = (value !== undefined && value !== null) ? value : 1.0; if (this._state.roughness === value) { return; } this._state.roughness = value; this.glRedraw(); } /** * Gets the roughness factor. * * @type {Number} */ get roughness() { return this._state.roughness; } /** * Gets the RGB {@link Texture} containing this MetallicMaterial's roughness factor in its *R* component. * * The *R* component multiplies by {@link MetallicMaterial#roughness}. * * @type {Texture} */ get roughnessMap() { return this._attached.roughnessMap; } /** * Gets the RGB {@link Texture} containing this MetallicMaterial's metalness in its *R* component and roughness in its *G* component. * * Its *B* component multiplies by the {@link MetallicMaterial#metallic} property, while its *G* component multiplies by the {@link MetallicMaterial#roughness} property. * * @type {Texture} */ get metallicRoughnessMap() { return this._attached.metallicRoughnessMap; } /** * Sets the factor in the range [0..1] indicating specular Fresnel value. * * Default value is ````0.0````. * * @type {Number} */ set specularF0(value) { value = (value !== undefined && value !== null) ? value : 0.0; if (this._state.specularF0 === value) { return; } this._state.specularF0 = value; this.glRedraw(); } /** * Gets the factor in the range [0..1] indicating specular Fresnel value. * * @type {Number} */ get specularF0() { return this._state.specularF0; } /** * Sets the RGB emissive color. * * Multiplies by {@link MetallicMaterial#emissiveMap}. * * Default value is ````[0.0, 0.0, 0.0]````. * * @type {Number[]} */ set emissive(value) { let emissive = this._state.emissive; if (!emissive) { emissive = this._state.emissive = new Float32Array(3); } else if (value && emissive[0] === value[0] && emissive[1] === value[1] && emissive[2] === value[2]) { return; } if (value) { emissive[0] = value[0]; emissive[1] = value[1]; emissive[2] = value[2]; } else { emissive[0] = 0; emissive[1] = 0; emissive[2] = 0; } this.glRedraw(); } /** * Gets the RGB emissive color. * * @type {Number[]} */ get emissive() { return this._state.emissive; } /** * Gets the RGB emissive map. * * Multiplies by {@link MetallicMaterial#emissive}. * * @type {Texture} */ get emissiveMap() { return this._attached.emissiveMap; } /** * Gets the RGB ambient occlusion map. * * Multiplies by the specular and diffuse light reflected by surfaces. * * @type {Texture} */ get occlusionMap() { return this._attached.occlusionMap; } /** * Sets factor in the range ````[0..1]```` that indicates the alpha value. * * Multiplies by the *R* component of {@link MetallicMaterial#alphaMap} and the *A* component, if present, of {@link MetallicMaterial#baseColorMap}. * * The value of {@link MetallicMaterial#alphaMode} indicates how alpha is interpreted when rendering. * * Default value is ````1.0````. * * @type {Number} */ set alpha(value) { value = (value !== undefined && value !== null) ? value : 1.0; if (this._state.alpha === value) { return; } this._state.alpha = value; this.glRedraw(); } /** * Gets factor in the range ````[0..1]```` that indicates the alpha value. * * @type {Number} */ get alpha() { return this._state.alpha; } /** * Gets the RGB {@link Texture} containing this MetallicMaterial's alpha in its *R* component. * * The *R* component multiplies by the {@link MetallicMaterial#alpha} property. * * @type {Texture} */ get alphaMap() { return this._attached.alphaMap; } /** * Gets the RGB tangent-space normal map {@link Texture}. * * @type {Texture} */ get normalMap() { return this._attached.normalMap; } /** * Sets the alpha rendering mode. * * This specifies how alpha is interpreted. Alpha is the combined result of the {@link MetallicMaterial#alpha} and {@link MetallicMaterial#alphaMap} properties. * * Accepted values are: * * * "opaque" - The alpha value is ignored and the rendered output is fully opaque (default). * * "mask" - The rendered output is either fully opaque or fully transparent depending on the alpha and {@link MetallicMaterial#alphaCutoff}. * * "blend" - The alpha value is used to composite the source and destination areas. The rendered output is combined with the background using the normal painting operation (i.e. the Porter and Duff over operator). * * @type {String} */ set alphaMode(alphaMode) { alphaMode = alphaMode || "opaque"; let value = modes[alphaMode]; if (value === undefined) { this.error("Unsupported value for 'alphaMode': " + alphaMode + " defaulting to 'opaque'"); value = "opaque"; } if (this._state.alphaMode === value) { return; } this._state.alphaMode = value; this.glRedraw(); } /** * Gets the alpha rendering mode. * * @type {String} */ get alphaMode() { return modeNames[this._state.alphaMode]; } /** * Sets the alpha cutoff value. * * Specifies the cutoff threshold when {@link MetallicMaterial#alphaMode} equals "mask". If the alpha is greater than or equal to this value then it is rendered as fully opaque, otherwise, it is rendered as fully transparent. A value greater than 1.0 will render the entire * material as fully transparent. This value is ignored for other modes. * * Alpha is the combined result of the {@link MetallicMaterial#alpha} and {@link MetallicMaterial#alphaMap} properties. * * Default value is ````0.5````. * * @type {Number} */ set alphaCutoff(alphaCutoff) { if (alphaCutoff === null || alphaCutoff === undefined) { alphaCutoff = 0.5; } if (this._state.alphaCutoff === alphaCutoff) { return; } this._state.alphaCutoff = alphaCutoff; } /** * Gets the alpha cutoff value. * * @type {Number} */ get alphaCutoff() { return this._state.alphaCutoff; } /** * Sets whether backfaces are visible on attached {@link Mesh}es. * * The backfaces will belong to {@link ReadableGeometry} compoents that are also attached to the {@link Mesh}es. * * Default is ````false````. * * @type {Boolean} */ set backfaces(value) { value = !!value; if (this._state.backfaces === value) { return; } this._state.backfaces = value; this.glRedraw(); } /** * Gets whether backfaces are visible on attached {@link Mesh}es. * * @type {Boolean} */ get backfaces() { return this._state.backfaces; } /** * Sets the winding direction of front faces of {@link Geometry} of attached {@link Mesh}es. * * Default value is ````"ccw"````. * * @type {String} */ set frontface(value) { value = value !== "cw"; if (this._state.frontface === value) { return; } this._state.frontface = value; this.glRedraw(); } /** * Gets the winding direction of front faces of {@link Geometry} of attached {@link Mesh}es. * * @type {String} */ get frontface() { return this._state.frontface ? "ccw" : "cw"; } /** * Sets the MetallicMaterial's line width. * * This is not supported by WebGL implementations based on DirectX [2019]. * * Default value is ````1.0````. * * @type {Number} */ set lineWidth(value) { this._state.lineWidth = value || 1.0; this.glRedraw(); } /** * Gets the MetallicMaterial's line width. * * @type {Number} */ get lineWidth() { return this._state.lineWidth; } /** * Sets the MetallicMaterial's point size. * * Default value is ````1.0````. * * @type {Number} */ set pointSize(value) { this._state.pointSize = value || 1.0; this.glRedraw(); } /** * Gets the MetallicMaterial's point size. * * @type {Number} */ get pointSize() { return this._state.pointSize; } /** * Destroys this MetallicMaterial. */ destroy() { super.destroy(); this._state.destroy(); } } const alphaModes = {"opaque": 0, "mask": 1, "blend": 2}; const alphaModeNames = ["opaque", "mask", "blend"]; /** * @desc Configures the normal rendered appearance of {@link Mesh}es using the physically-accurate *specular-glossiness* shading model. * * * Useful for insulators, such as wood, ceramics and plastic. * * {@link MetallicMaterial} is best for conductive materials, such as metal. * * {@link PhongMaterial} is appropriate for non-realistic objects. * * {@link LambertMaterial} is appropriate for high-detail models that need to render as efficiently as possible. * * ## Usage * * In the example below we'll create a {@link Mesh} with a {@link buildTorusGeometry} and a SpecularMaterial. * * Note that in this example we're providing separate {@link Texture} for the {@link SpecularMaterial#specular} and {@link SpecularMaterial#glossiness} * channels, which allows us a little creative flexibility. Then, in the next example further down, we'll combine those channels * within the same {@link Texture} for efficiency. * * ````javascript * import {Viewer, Mesh, buildTorusGeometry, SpecularMaterial, Texture} from "xeokit-sdk.es.js"; * * const viewer = new Viewer({ canvasId: "myCanvas" }); * * const myMesh = new Mesh(viewer.scene,{ * * geometry: new ReadableGeometry(viewer.scene, buildTorusGeometry()), * * material: new SpecularMaterial(viewer.scene,{ * * // Channels with default values, just to show them * * diffuse: [1.0, 1.0, 1.0], * specular: [1.0, 1.0, 1.0], * glossiness: 1.0, * emissive: [0.0, 0.0, 0.0] * alpha: 1.0, * * // Textures to multiply some of the channels * * diffuseMap: new Texture(viewer.scene, { // RGB components multiply by diffuse * src: "textures/diffuse.jpg" * }), * specularMap: new Texture(viewer.scene, { // RGB component multiplies by specular * src: "textures/specular.jpg" * }), * glossinessMap: new Texture(viewer.scene, { // R component multiplies by glossiness * src: "textures/glossiness.jpg" * }), * normalMap: new Texture(viewer.scene, { * src: "textures/normalMap.jpg" * }) * }) * }); * ```` * * ## Combining Channels Within the Same Textures * * In the previous example we provided separate {@link Texture} for the {@link SpecularMaterial#specular} and * {@link SpecularMaterial#glossiness} channels, but we can combine those channels into the same {@link Texture} to reduce * download time, memory footprint and rendering time (and also for glTF compatibility). * * Here's our SpecularMaterial again with those channels combined in the {@link SpecularMaterial#specularGlossinessMap} * {@link Texture}, where the *RGB* component multiplies by {@link SpecularMaterial#specular} and *A* multiplies by {@link SpecularMaterial#glossiness}. * * ````javascript * const myMesh = new Mesh(viewer.scene,{ * * geometry: new ReadableGeometry(viewer.scene, buildTorusGeometry()), * * material: new SpecularMaterial(viewer.scene,{ * * // Channels with default values, just to show them * * diffuse: [1.0, 1.0, 1.0], * specular: [1.0, 1.0, 1.0], * glossiness: 1.0, * emissive: [0.0, 0.0, 0.0] * alpha: 1.0, * * diffuseMap: new Texture(viewer.scene, { * src: "textures/diffuse.jpg" * }), * specularGlossinessMap: new Texture(viewer.scene, { // RGB multiplies by specular, A by glossiness * src: "textures/specularGlossiness.jpg" * }), * normalMap: new Texture(viewer.scene, { * src: "textures/normalMap.jpg" * }) * }) * }); * ```` * * Although not shown in this example, we can also texture {@link SpecularMaterial#alpha} with * the *A* component of {@link SpecularMaterial#diffuseMap}'s {@link Texture}, if required. * * ## Alpha Blending * * Let's make our {@link Mesh} transparent. We'll redefine {@link SpecularMaterial#alpha} * and {@link SpecularMaterial#alphaMode}, causing it to blend 50% with the background: * * ````javascript * const myMesh = new Mesh(viewer.scene,{ * * geometry: new ReadableGeometry(viewer.scene, buildTorusGeometry()), * * material: new SpecularMaterial(viewer.scene,{ * * // Channels with default values, just to show them * * diffuse: [1.0, 1.0, 1.0], * specular: [1.0, 1.0, 1.0], * glossiness: 1.0, * emissive: [0.0, 0.0, 0.0] * alpha: 0.5, // <<----------- Changed * alphaMode: "blend", // <<----------- Added * * diffuseMap: new Texture(viewer.scene, { * src: "textures/diffuse.jpg" * }), * specularGlossinessMap: new Texture(viewer.scene, { // RGB multiplies by specular, A by glossiness * src: "textures/specularGlossiness.jpg" * }), * normalMap: new Texture(viewer.scene, { * src: "textures/normalMap.jpg" * }) * }) * }); * ```` * * ## Alpha Masking * * Now let's make holes in our {@link Mesh}. We'll give its SpecularMaterial an {@link SpecularMaterial#alphaMap} * and configure {@link SpecularMaterial#alpha}, {@link SpecularMaterial#alphaMode}, * and {@link SpecularMaterial#alphaCutoff} to treat it as an alpha mask: * * ````javascript * const myMesh = new Mesh(viewer.scene,{ * * geometry: buildTorusGeometry(viewer.scene, ReadableGeometry, {}), * * material: new SpecularMaterial(viewer.scene, { * * // Channels with default values, just to show them * * diffuse: [1.0, 1.0, 1.0], * specular: [1.0, 1.0, 1.0], * glossiness: 1.0, * emissive: [0.0, 0.0, 0.0] * alpha: 1.0, // <<----------- Changed * alphaMode: "mask", // <<----------- Changed * alphaCutoff: 0.2, // <<----------- Added * * alphaMap: new Texture(viewer.scene, { // <<---------- Added * src: "textures/diffuse/crossGridColorMap.jpg" * }), * diffuseMap: new Texture(viewer.scene, { * src: "textures/diffuse.jpg" * }), * specularGlossinessMap: new Texture(viewer.scene, { // RGB multiplies by specular, A by glossiness * src: "textures/specularGlossiness.jpg" * }), * normalMap: new Texture(viewer.scene, { * src: "textures/normalMap.jpg" * }) * }) * }); * ```` * * ## Background Theory * * For an introduction to physically-based rendering (PBR) concepts, try these articles: * * * Joe Wilson's [Basic Theory of Physically-Based Rendering](https://www.marmoset.co/posts/basic-theory-of-physically-based-rendering/) * * Jeff Russel's [Physically-based Rendering, and you can too!](https://www.marmoset.co/posts/physically-based-rendering-and-you-can-too/) * * Sebastien Legarde's [Adapting a physically-based shading model](http://seblagarde.wordpress.com/tag/physically-based-rendering/) * * ## SpecularMaterial Properties * * The following table summarizes SpecularMaterial properties: * * | Property | Type | Range | Default Value | Space | Description | * |:--------:|:----:|:-----:|:-------------:|:-----:|:-----------:| * | {@link SpecularMaterial#diffuse} | Array | [0, 1] for all components | [1,1,1,1] | linear | The RGB components of the diffuse color of the material. | * | {@link SpecularMaterial#specular} | Array | [0, 1] for all components | [1,1,1,1] | linear | The RGB components of the specular color of the material. | * | {@link SpecularMaterial#glossiness} | Number | [0, 1] | 1 | linear | The glossiness the material. | * | {@link SpecularMaterial#specularF0} | Number | [0, 1] | 1 | linear | The specularF0 of the material surface. | * | {@link SpecularMaterial#emissive} | Array | [0, 1] for all components | [0,0,0] | linear | The RGB components of the emissive color of the material. | * | {@link SpecularMaterial#alpha} | Number | [0, 1] | 1 | linear | The transparency of the material surface (0 fully transparent, 1 fully opaque). | * | {@link SpecularMaterial#diffuseMap} | {@link Texture} | | null | sRGB | Texture RGB components multiplying by {@link SpecularMaterial#diffuse}. If the fourth component (A) is present, it multiplies by {@link SpecularMaterial#alpha}. | * | {@link SpecularMaterial#specularMap} | {@link Texture} | | null | sRGB | Texture RGB components multiplying by {@link SpecularMaterial#specular}. If the fourth component (A) is present, it multiplies by {@link SpecularMaterial#alpha}. | * | {@link SpecularMaterial#glossinessMap} | {@link Texture} | | null | linear | Texture with first component multiplying by {@link SpecularMaterial#glossiness}. | * | {@link SpecularMaterial#specularGlossinessMap} | {@link Texture} | | null | linear | Texture with first three components multiplying by {@link SpecularMaterial#specular} and fourth component multiplying by {@link SpecularMaterial#glossiness}. | * | {@link SpecularMaterial#emissiveMap} | {@link Texture} | | null | linear | Texture with RGB components multiplying by {@link SpecularMaterial#emissive}. | * | {@link SpecularMaterial#alphaMap} | {@link Texture} | | null | linear | Texture with first component multiplying by {@link SpecularMaterial#alpha}. | * | {@link SpecularMaterial#occlusionMap} | {@link Texture} | | null | linear | Ambient occlusion texture multiplying by surface's reflected diffuse and specular light. | * | {@link SpecularMaterial#normalMap} | {@link Texture} | | null | linear | Tangent-space normal map. | * | {@link SpecularMaterial#alphaMode} | String | "opaque", "blend", "mask" | "blend" | | Alpha blend mode. | * | {@link SpecularMaterial#alphaCutoff} | Number | [0..1] | 0.5 | | Alpha cutoff value. | * | {@link SpecularMaterial#backfaces} | Boolean | | false | | Whether to render {@link Geometry} backfaces. | * | {@link SpecularMaterial#frontface} | String | "ccw", "cw" | "ccw" | | The winding order for {@link Geometry} frontfaces - "cw" for clockwise, or "ccw" for counter-clockwise. | * */ class SpecularMaterial extends Material { /** @private */ get type() { return "SpecularMaterial"; } /** * * @constructor * @param {Component} owner Owner component. When destroyed, the owner will destroy this component as well. * @param {*} [cfg] The SpecularMaterial configuration * @param {String} [cfg.id] Optional ID, unique among all components in the parent {@link Scene}, generated automatically when omitted. * @param {Number[]} [cfg.diffuse=[1,1,1]] RGB diffuse color of this SpecularMaterial. Multiplies by the RGB components of {@link SpecularMaterial#diffuseMap}. * @param {Texture} [cfg.diffuseMap=undefined] RGBA {@link Texture} containing the diffuse color of this SpecularMaterial, with optional *A* component for alpha. The RGB components multiply by {@link SpecularMaterial#diffuse}, while the *A* component, if present, multiplies by {@link SpecularMaterial#alpha}. * @param {Number} [cfg.specular=[1,1,1]] RGB specular color of this SpecularMaterial. Multiplies by the {@link SpecularMaterial#specularMap} and the *RGB* components of {@link SpecularMaterial#specularGlossinessMap}. * @param {Texture} [cfg.specularMap=undefined] RGB texture containing the specular color of this SpecularMaterial. Multiplies by the {@link SpecularMaterial#specular} property. Must be within the same {@link Scene} as this SpecularMaterial. * @param {Number} [cfg.glossiness=1.0] Factor in the range [0..1] indicating how glossy this SpecularMaterial is. 0 is no glossiness, 1 is full glossiness. Multiplies by the *R* component of {@link SpecularMaterial#glossinessMap} and the *A* component of {@link SpecularMaterial#specularGlossinessMap}. * @param {Texture} [cfg.specularGlossinessMap=undefined] RGBA {@link Texture} containing this SpecularMaterial's specular color in its *RGB* component and glossiness in its *A* component. Its *RGB* components multiply by {@link SpecularMaterial#specular}, while its *A* component multiplies by {@link SpecularMaterial#glossiness}. Must be within the same {@link Scene} as this SpecularMaterial. * @param {Number} [cfg.specularF0=0.0] Factor in the range 0..1 indicating how reflective this SpecularMaterial is. * @param {Number[]} [cfg.emissive=[0,0,0]] RGB emissive color of this SpecularMaterial. Multiplies by the RGB components of {@link SpecularMaterial#emissiveMap}. * @param {Texture} [cfg.emissiveMap=undefined] RGB {@link Texture} containing the emissive color of this SpecularMaterial. Multiplies by the {@link SpecularMaterial#emissive} property. Must be within the same {@link Scene} as this SpecularMaterial. * @param {Texture} [cfg.occlusionMap=undefined] RGB ambient occlusion {@link Texture}. Within shaders, multiplies by the specular and diffuse light reflected by surfaces. Must be within the same {@link Scene} as this SpecularMaterial. * @param {Texture} [cfg.normalMap=undefined] {Texture} RGB tangent-space normal {@link Texture}. Must be within the same {@link Scene} as this SpecularMaterial. * @param {Number} [cfg.alpha=1.0] Factor in the range 0..1 indicating how transparent this SpecularMaterial is. A value of 0.0 indicates fully transparent, 1.0 is fully opaque. Multiplies by the *R* component of {@link SpecularMaterial#alphaMap} and the *A* component, if present, of {@link SpecularMaterial#diffuseMap}. * @param {Texture} [cfg.alphaMap=undefined] RGB {@link Texture} containing this SpecularMaterial's alpha in its *R* component. The *R* component multiplies by the {@link SpecularMaterial#alpha} property. Must be within the same {@link Scene} as this SpecularMaterial. * @param {String} [cfg.alphaMode="opaque"] The alpha blend mode - accepted values are "opaque", "blend" and "mask". See the {@link SpecularMaterial#alphaMode} property for more info. * @param {Number} [cfg.alphaCutoff=0.5] The alpha cutoff value. See the {@link SpecularMaterial#alphaCutoff} property for more info. * @param {Boolean} [cfg.backfaces=false] Whether to render {@link Geometry} backfaces. * @param {Boolean} [cfg.frontface="ccw"] The winding order for {@link Geometry} front faces - "cw" for clockwise, or "ccw" for counter-clockwise. * @param {Number} [cfg.lineWidth=1] Scalar that controls the width of {@link Geometry lines. * @param {Number} [cfg.pointSize=1] Scalar that controls the size of {@link Geometry} points. */ constructor(owner, cfg = {}) { super(owner, cfg); this._state = new RenderState({ type: "SpecularMaterial", diffuse: math.vec3([1.0, 1.0, 1.0]), emissive: math.vec3([0.0, 0.0, 0.0]), specular: math.vec3([1.0, 1.0, 1.0]), glossiness: null, specularF0: null, alpha: null, alphaMode: null, alphaCutoff: null, lineWidth: null, pointSize: null, backfaces: null, frontface: null, // Boolean for speed; true == "ccw", false == "cw" hash: null }); this.diffuse = cfg.diffuse; this.specular = cfg.specular; this.glossiness = cfg.glossiness; this.specularF0 = cfg.specularF0; this.emissive = cfg.emissive; this.alpha = cfg.alpha; if (cfg.diffuseMap) { this._diffuseMap = this._checkComponent("Texture", cfg.diffuseMap); } if (cfg.emissiveMap) { this._emissiveMap = this._checkComponent("Texture", cfg.emissiveMap); } if (cfg.specularMap) { this._specularMap = this._checkComponent("Texture", cfg.specularMap); } if (cfg.glossinessMap) { this._glossinessMap = this._checkComponent("Texture", cfg.glossinessMap); } if (cfg.specularGlossinessMap) { this._specularGlossinessMap = this._checkComponent("Texture", cfg.specularGlossinessMap); } if (cfg.occlusionMap) { this._occlusionMap = this._checkComponent("Texture", cfg.occlusionMap); } if (cfg.alphaMap) { this._alphaMap = this._checkComponent("Texture", cfg.alphaMap); } if (cfg.normalMap) { this._normalMap = this._checkComponent("Texture", cfg.normalMap); } this.alphaMode = cfg.alphaMode; this.alphaCutoff = cfg.alphaCutoff; this.backfaces = cfg.backfaces; this.frontface = cfg.frontface; this.lineWidth = cfg.lineWidth; this.pointSize = cfg.pointSize; this._makeHash(); } _makeHash() { const state = this._state; const hash = ["/spe"]; if (this._diffuseMap) { hash.push("/dm"); if (this._diffuseMap.hasMatrix) { hash.push("/mat"); } hash.push("/" + this._diffuseMap.encoding); } if (this._emissiveMap) { hash.push("/em"); if (this._emissiveMap.hasMatrix) { hash.push("/mat"); } } if (this._glossinessMap) { hash.push("/gm"); if (this._glossinessMap.hasMatrix) { hash.push("/mat"); } } if (this._specularMap) { hash.push("/sm"); if (this._specularMap.hasMatrix) { hash.push("/mat"); } } if (this._specularGlossinessMap) { hash.push("/sgm"); if (this._specularGlossinessMap.hasMatrix) { hash.push("/mat"); } } if (this._occlusionMap) { hash.push("/ocm"); if (this._occlusionMap.hasMatrix) { hash.push("/mat"); } } if (this._normalMap) { hash.push("/nm"); if (this._normalMap.hasMatrix) { hash.push("/mat"); } } if (this._alphaMap) { hash.push("/opm"); if (this._alphaMap.hasMatrix) { hash.push("/mat"); } } hash.push(";"); state.hash = hash.join(""); } /** * Sets the RGB diffuse color of this SpecularMaterial. * * Multiplies by the *RGB* components of {@link SpecularMaterial#diffuseMap}. * * Default value is ````[1.0, 1.0, 1.0]````. * @type {Number[]} */ set diffuse(value) { let diffuse = this._state.diffuse; if (!diffuse) { diffuse = this._state.diffuse = new Float32Array(3); } else if (value && diffuse[0] === value[0] && diffuse[1] === value[1] && diffuse[2] === value[2]) { return; } if (value) { diffuse[0] = value[0]; diffuse[1] = value[1]; diffuse[2] = value[2]; } else { diffuse[0] = 1; diffuse[1] = 1; diffuse[2] = 1; } this.glRedraw(); } /** * Gets the RGB diffuse color of this SpecularMaterial. * * @type {Number[]} */ get diffuse() { return this._state.diffuse; } /** * Gets the RGB {@link Texture} containing the diffuse color of this SpecularMaterial, with optional *A* component for alpha. * * The *RGB* components multipliues by the {@link SpecularMaterial#diffuse} property, while the *A* component, if present, multiplies by the {@link SpecularMaterial#alpha} property. * * @type {Texture} */ get diffuseMap() { return this._diffuseMap; } /** * Sets the RGB specular color of this SpecularMaterial. * * Multiplies by {@link SpecularMaterial#specularMap} and the *A* component of {@link SpecularMaterial#specularGlossinessMap}. * * Default value is ````[1.0, 1.0, 1.0]````. * * @type {Number[]} */ set specular(value) { let specular = this._state.specular; if (!specular) { specular = this._state.specular = new Float32Array(3); } else if (value && specular[0] === value[0] && specular[1] === value[1] && specular[2] === value[2]) { return; } if (value) { specular[0] = value[0]; specular[1] = value[1]; specular[2] = value[2]; } else { specular[0] = 1; specular[1] = 1; specular[2] = 1; } this.glRedraw(); } /** * Gets the RGB specular color of this SpecularMaterial. * * @type {Number[]} */ get specular() { return this._state.specular; } /** * Gets the RGB texture containing the specular color of this SpecularMaterial. * * Multiplies by {@link SpecularMaterial#specular}. * * @type {Texture} */ get specularMap() { return this._specularMap; } /** * Gets the RGBA texture containing this SpecularMaterial's specular color in its *RGB* components and glossiness in its *A* component. * * The *RGB* components multiplies {@link SpecularMaterial#specular}, while the *A* component multiplies by {@link SpecularMaterial#glossiness}. * * @type {Texture} */ get specularGlossinessMap() { return this._specularGlossinessMap; } /** * Sets the Factor in the range [0..1] indicating how glossy this SpecularMaterial is. * * ````0```` is no glossiness, ````1```` is full glossiness. * * Multiplies by the *R* component of {@link SpecularMaterial#glossinessMap} and the *A* component of {@link SpecularMaterial#specularGlossinessMap}. * * Default value is ````1.0````. * * @type {Number} */ set glossiness(value) { value = (value !== undefined && value !== null) ? value : 1.0; if (this._state.glossiness === value) { return; } this._state.glossiness = value; this.glRedraw(); } /** * Gets the Factor in the range ````[0..1]```` indicating how glossy this SpecularMaterial is. * @type {Number} */ get glossiness() { return this._state.glossiness; } /** * Gets the RGB texture containing this SpecularMaterial's glossiness in its *R* component. * * The *R* component multiplies by {@link SpecularMaterial#glossiness}. ** @type {Texture} */ get glossinessMap() { return this._glossinessMap; } /** * Sets the factor in the range ````[0..1]```` indicating amount of specular Fresnel. * * Default value is ````0.0````. * * @type {Number} */ set specularF0(value) { value = (value !== undefined && value !== null) ? value : 0.0; if (this._state.specularF0 === value) { return; } this._state.specularF0 = value; this.glRedraw(); } /** * Gets the factor in the range ````[0..1]```` indicating amount of specular Fresnel. * * @type {Number} */ get specularF0() { return this._state.specularF0; } /** * Sets the RGB emissive color of this SpecularMaterial. * * Multiplies by {@link SpecularMaterial#emissiveMap}. * Default value is ````[0.0, 0.0, 0.0]````. * * @type {Number[]} */ set emissive(value) { let emissive = this._state.emissive; if (!emissive) { emissive = this._state.emissive = new Float32Array(3); } else if (value && emissive[0] === value[0] && emissive[1] === value[1] && emissive[2] === value[2]) { return; } if (value) { emissive[0] = value[0]; emissive[1] = value[1]; emissive[2] = value[2]; } else { emissive[0] = 0; emissive[1] = 0; emissive[2] = 0; } this.glRedraw(); } /** * Gets the RGB emissive color of this SpecularMaterial. * * @type {Number[]} */ get emissive() { return this._state.emissive; } /** * Gets the RGB texture containing the emissive color of this SpecularMaterial. * * Multiplies by {@link SpecularMaterial#emissive}. * * @type {Texture} */ get emissiveMap() { return this._emissiveMap; } /** * Sets the factor in the range [0..1] indicating how transparent this SpecularMaterial is. * * A value of ````0.0```` is fully transparent, while ````1.0```` is fully opaque. * * Multiplies by the *R* component of {@link SpecularMaterial#alphaMap} and the *A* component, if present, of {@link SpecularMaterial#diffuseMap}. * * Default value is ````1.0````. * * @type {Number} */ set alpha(value) { value = (value !== undefined && value !== null) ? value : 1.0; if (this._state.alpha === value) { return; } this._state.alpha = value; this.glRedraw(); } /** * Gets the factor in the range [0..1] indicating how transparent this SpecularMaterial is. * * @type {Number} */ get alpha() { return this._state.alpha; } /** * Gets the RGB {@link Texture} with alpha in its *R* component. * * The *R* component multiplies by the {@link SpecularMaterial#alpha} property. * * @type {Texture} */ get alphaMap() { return this._alphaMap; } /** * Gets the RGB tangent-space normal {@link Texture} attached to this SpecularMaterial. * * @type {Texture} */ get normalMap() { return this._normalMap; } /** * Gets the RGB ambient occlusion {@link Texture} attached to this SpecularMaterial. * * Multiplies by the specular and diffuse light reflected by surfaces. * * @type {Texture} */ get occlusionMap() { return this._occlusionMap; } /** * Sets the alpha rendering mode. * * This governs how alpha is treated. Alpha is the combined result of the {@link SpecularMaterial#alpha} and {@link SpecularMaterial#alphaMap} properties. * * Accepted values are: * * * "opaque" - The alpha value is ignored and the rendered output is fully opaque (default). * * "mask" - The rendered output is either fully opaque or fully transparent depending on the alpha value and the specified alpha cutoff value. * * "blend" - The alpha value is used to composite the source and destination areas. The rendered output is combined with the background using the normal painting operation (i.e. the Porter and Duff over operator) * * @type {String} */ set alphaMode(alphaMode) { alphaMode = alphaMode || "opaque"; let value = alphaModes[alphaMode]; if (value === undefined) { this.error("Unsupported value for 'alphaMode': " + alphaMode + " defaulting to 'opaque'"); value = "opaque"; } if (this._state.alphaMode === value) { return; } this._state.alphaMode = value; this.glRedraw(); } get alphaMode() { return alphaModeNames[this._state.alphaMode]; } /** * Sets the alpha cutoff value. * * Specifies the cutoff threshold when {@link SpecularMaterial#alphaMode} equals "mask". If the alpha is greater than or equal to this value then it is rendered as fully opaque, otherwise, it is rendered as fully transparent. A value greater than 1.0 will render the entire material as fully transparent. This value is ignored for other modes. * * Alpha is the combined result of the {@link SpecularMaterial#alpha} and {@link SpecularMaterial#alphaMap} properties. * * Default value is ````0.5````. * * @type {Number} */ set alphaCutoff(alphaCutoff) { if (alphaCutoff === null || alphaCutoff === undefined) { alphaCutoff = 0.5; } if (this._state.alphaCutoff === alphaCutoff) { return; } this._state.alphaCutoff = alphaCutoff; } /** * Gets the alpha cutoff value. * @type {Number} */ get alphaCutoff() { return this._state.alphaCutoff; } /** * Sets whether backfaces are visible on attached {@link Mesh}es. * * The backfaces will belong to {@link ReadableGeometry} compoents that are also attached to the {@link Mesh}es. * * Default is ````false````. * * @type {Boolean} */ set backfaces(value) { value = !!value; if (this._state.backfaces === value) { return; } this._state.backfaces = value; this.glRedraw(); } /** * Gets whether backfaces are visible on attached {@link Mesh}es. * * @type {Boolean} */ get backfaces() { return this._state.backfaces; } /** * Sets the winding direction of front faces of {@link Geometry} of attached {@link Mesh}es. * * Default value is ````"ccw"````. * * @type {String} */ set frontface(value) { value = value !== "cw"; if (this._state.frontface === value) { return; } this._state.frontface = value; this.glRedraw(); } /** * Gets the winding direction of front faces of {@link Geometry} of attached {@link Mesh}es. * * @type {String} */ get frontface() { return this._state.frontface ? "ccw" : "cw"; } /** * Sets the SpecularMaterial's line width. * * This is not supported by WebGL implementations based on DirectX [2019]. * * Default value is ````1.0````. * * @type {Number} */ set lineWidth(value) { this._state.lineWidth = value || 1.0; this.glRedraw(); } /** * Gets the SpecularMaterial's line width. * * @type {Number} */ get lineWidth() { return this._state.lineWidth; } /** * Sets the SpecularMaterial's point size. * * Default value is ````1.0````. * * @type {Number} */ set pointSize(value) { this._state.pointSize = value || 1; this.glRedraw(); } /** * Sets the SpecularMaterial's point size. * * @type {Number} */ get pointSize() { return this._state.pointSize; } /** * Destroys this SpecularMaterial. */ destroy() { super.destroy(); this._state.destroy(); } } const PRESETS$3 = { "default": { fill: true, fillColor: [0.4, 0.4, 0.4], fillAlpha: 0.2, edges: true, edgeColor: [0.2, 0.2, 0.2], edgeAlpha: 0.5, edgeWidth: 1 }, "defaultWhiteBG": { fill: true, fillColor: [1, 1, 1], fillAlpha: 0.6, edgeColor: [0.2, 0.2, 0.2], edgeAlpha: 1.0, edgeWidth: 1 }, "defaultLightBG": { fill: true, fillColor: [0.4, 0.4, 0.4], fillAlpha: 0.2, edges: true, edgeColor: [0.2, 0.2, 0.2], edgeAlpha: 0.5, edgeWidth: 1 }, "defaultDarkBG": { fill: true, fillColor: [0.4, 0.4, 0.4], fillAlpha: 0.2, edges: true, edgeColor: [0.5, 0.5, 0.5], edgeAlpha: 0.5, edgeWidth: 1 }, "phosphorous": { fill: true, fillColor: [0.0, 0.0, 0.0], fillAlpha: 0.4, edges: true, edgeColor: [0.9, 0.9, 0.9], edgeAlpha: 0.5, edgeWidth: 2 }, "sunset": { fill: true, fillColor: [0.9, 0.9, 0.6], fillAlpha: 0.2, edges: true, edgeColor: [0.9, 0.9, 0.9], edgeAlpha: 0.5, edgeWidth: 1 }, "vectorscope": { fill: true, fillColor: [0.0, 0.0, 0.0], fillAlpha: 0.7, edges: true, edgeColor: [0.2, 1.0, 0.2], edgeAlpha: 1, edgeWidth: 2 }, "battlezone": { fill: true, fillColor: [0.0, 0.0, 0.0], fillAlpha: 1.0, edges: true, edgeColor: [0.2, 1.0, 0.2], edgeAlpha: 1, edgeWidth: 3 }, "sepia": { fill: true, fillColor: [0.970588207244873, 0.7965892553329468, 0.6660899519920349], fillAlpha: 0.4, edges: true, edgeColor: [0.529411792755127, 0.4577854573726654, 0.4100345969200134], edgeAlpha: 1.0, edgeWidth: 1 }, "yellowHighlight": { fill: true, fillColor: [1.0, 1.0, 0.0], fillAlpha: 0.5, edges: true, edgeColor: [1.0, 1.0, 1.0], edgeAlpha: 1.0, edgeWidth: 1 }, "greenSelected": { fill: true, fillColor: [0.0, 1.0, 0.0], fillAlpha: 0.5, edges: true, edgeColor: [1.0, 1.0, 1.0], edgeAlpha: 1.0, edgeWidth: 1 }, "gamegrid": { fill: true, fillColor: [0.2, 0.2, 0.7], fillAlpha: 0.9, edges: true, edgeColor: [0.4, 0.4, 1.6], edgeAlpha: 0.8, edgeWidth: 3 } }; /** * Configures the appearance of {@link Entity}s when they are xrayed, highlighted or selected. * * * XRay an {@link Entity} by setting {@link Entity#xrayed} ````true````. * * Highlight an {@link Entity} by setting {@link Entity#highlighted} ````true````. * * Select an {@link Entity} by setting {@link Entity#selected} ````true````. * * When {@link Entity}s are within the subtree of a root {@link Entity}, then setting {@link Entity#xrayed}, {@link Entity#highlighted} or {@link Entity#selected} * on the root will collectively set those properties on all sub-{@link Entity}s. * * EmphasisMaterial provides several presets. Select a preset by setting {@link EmphasisMaterial#preset} to the ID of a preset in {@link EmphasisMaterial#presets}. * * By default, a {@link Mesh} uses the default EmphasisMaterials in {@link Scene#xrayMaterial}, {@link Scene#highlightMaterial} and {@link Scene#selectedMaterial} * but you can assign each {@link Mesh#xrayMaterial}, {@link Mesh#highlightMaterial} or {@link Mesh#selectedMaterial} to a custom EmphasisMaterial, if required. * * ## Usage * * In the example below, we'll create a {@link Mesh} with its own XRayMaterial and set {@link Mesh#xrayed} ````true```` to xray it. * * Recall that {@link Mesh} is a concrete subtype of the abstract {@link Entity} base class. * * ````javascript * new Mesh(viewer.scene, { * geometry: new BoxGeometry(viewer.scene, { * edgeThreshold: 1 * }), * material: new PhongMaterial(viewer.scene, { * diffuse: [0.2, 0.2, 1.0] * }), * xrayMaterial: new EmphasisMaterial(viewer.scene, { * fill: true, * fillColor: [0, 0, 0], * fillAlpha: 0.7, * edges: true, * edgeColor: [0.2, 1.0, 0.2], * edgeAlpha: 1.0, * edgeWidth: 2 * }), * xrayed: true * }); * ```` * * Note the ````edgeThreshold```` configuration for the {@link ReadableGeometry} on our {@link Mesh}. EmphasisMaterial configures * a wireframe representation of the {@link ReadableGeometry} for the selected emphasis mode, which will have inner edges (those edges between * adjacent co-planar triangles) removed for visual clarity. The ````edgeThreshold```` indicates that, for * this particular {@link ReadableGeometry}, an inner edge is one where the angle between the surface normals of adjacent triangles * is not greater than ````5```` degrees. That's set to ````2```` by default, but we can override it to tweak the effect * as needed for particular Geometries. * * Here's the example again, this time implicitly defaulting to the {@link Scene#edgeMaterial}. We'll also modify that EdgeMaterial * to customize the effect. * * ````javascript * new Mesh({ * geometry: new TeapotGeometry(viewer.scene, { * edgeThreshold: 5 * }), * material: new PhongMaterial(viewer.scene, { * diffuse: [0.2, 0.2, 1.0] * }), * xrayed: true * }); * * var xrayMaterial = viewer.scene.xrayMaterial; * * xrayMaterial.fillColor = [0.2, 1.0, 0.2]; * xrayMaterial.fillAlpha = 1.0; * ```` * * ## Presets * * Let's switch the {@link Scene#xrayMaterial} to one of the presets in {@link EmphasisMaterial#presets}: * * ````javascript * viewer.xrayMaterial.preset = EmphasisMaterial.presets["sepia"]; * ```` * * We can also create an EmphasisMaterial from a preset, while overriding properties of the preset as required: * * ````javascript * var myEmphasisMaterial = new EMphasisMaterial(viewer.scene, { * preset: "sepia", * fillColor = [1.0, 0.5, 0.5] * }); * ```` */ class EmphasisMaterial extends Material { /** @private */ get type() { return "EmphasisMaterial"; } /** * Gets available EmphasisMaterial presets. * * @type {Object} */ get presets() { return PRESETS$3; }; /** * @constructor * @param {Component} owner Owner component. When destroyed, the owner will destroy this component as well. * @param {*} [cfg] The EmphasisMaterial configuration * @param {String} [cfg.id] Optional ID, unique among all components in the parent {@link Scene}, generated automatically when omitted. * @param {Boolean} [cfg.fill=true] Indicates if xray surfaces are filled with color. * @param {Number[]} [cfg.fillColor=[0.4,0.4,0.4]] EmphasisMaterial fill color. * @param {Number} [cfg.fillAlpha=0.2] Transparency of filled xray faces. A value of ````0.0```` indicates fully transparent, ````1.0```` is fully opaque. * @param {Boolean} [cfg.edges=true] Indicates if xray edges are visible. * @param {Number[]} [cfg.edgeColor=[0.2,0.2,0.2]] RGB color of xray edges. * @param {Number} [cfg.edgeAlpha=0.5] Transparency of xray edges. A value of ````0.0```` indicates fully transparent, ````1.0```` is fully opaque. * @param {Number} [cfg.edgeWidth=1] Width of xray edges, in pixels. * @param {String} [cfg.preset] Deprecated - will be removed in future releases - Selects a preset EmphasisMaterial configuration - see {@link EmphasisMaterial#presets}. * @param {Boolean} [cfg.backfaces=false] Whether to render geometry backfaces when emphasising. * @param {Boolean} [cfg.glowThrough=true] Whether to make the emphasized object appear to float on top of other objects, as if it were "glowing through" them. */ constructor(owner, cfg = {}) { super(owner, cfg); this._state = new RenderState({ type: "EmphasisMaterial", fill: null, fillColor: null, fillAlpha: null, edges: null, edgeColor: null, edgeAlpha: null, edgeWidth: null, backfaces: true, glowThrough: true }); this._preset = "default"; if (cfg.preset) { // Apply preset then override with configs where provided this.preset = cfg.preset; if (cfg.fill !== undefined) { this.fill = cfg.fill; } if (cfg.fillColor) { this.fillColor = cfg.fillColor; } if (cfg.fillAlpha !== undefined) { this.fillAlpha = cfg.fillAlpha; } if (cfg.edges !== undefined) { this.edges = cfg.edges; } if (cfg.edgeColor) { this.edgeColor = cfg.edgeColor; } if (cfg.edgeAlpha !== undefined) { this.edgeAlpha = cfg.edgeAlpha; } if (cfg.edgeWidth !== undefined) { this.edgeWidth = cfg.edgeWidth; } if (cfg.backfaces !== undefined) { this.backfaces = cfg.backfaces; } if (cfg.glowThrough !== undefined) { this.glowThrough = cfg.glowThrough; } } else { this.fill = cfg.fill; this.fillColor = cfg.fillColor; this.fillAlpha = cfg.fillAlpha; this.edges = cfg.edges; this.edgeColor = cfg.edgeColor; this.edgeAlpha = cfg.edgeAlpha; this.edgeWidth = cfg.edgeWidth; this.backfaces = cfg.backfaces; this.glowThrough = cfg.glowThrough; } } /** * Sets if surfaces are filled with color. * * Default is ````true````. * * @type {Boolean} */ set fill(value) { value = value !== false; if (this._state.fill === value) { return; } this._state.fill = value; this.glRedraw(); } /** * Gets if surfaces are filled with color. * * Default is ````true````. * * @type {Boolean} */ get fill() { return this._state.fill; } /** * Sets the RGB color of filled faces. * * Default is ````[0.4, 0.4, 0.4]````. * * @type {Number[]} */ set fillColor(value) { let fillColor = this._state.fillColor; if (!fillColor) { fillColor = this._state.fillColor = new Float32Array(3); } else if (value && fillColor[0] === value[0] && fillColor[1] === value[1] && fillColor[2] === value[2]) { return; } if (value) { fillColor[0] = value[0]; fillColor[1] = value[1]; fillColor[2] = value[2]; } else { fillColor[0] = 0.4; fillColor[1] = 0.4; fillColor[2] = 0.4; } this.glRedraw(); } /** * Gets the RGB color of filled faces. * * Default is ````[0.4, 0.4, 0.4]````. * * @type {Number[]} */ get fillColor() { return this._state.fillColor; } /** * Sets the transparency of filled faces. * * A value of ````0.0```` indicates fully transparent, ````1.0```` is fully opaque. * * Default is ````0.2````. * * @type {Number} */ set fillAlpha(value) { value = (value !== undefined && value !== null) ? value : 0.2; if (this._state.fillAlpha === value) { return; } this._state.fillAlpha = value; this.glRedraw(); } /** * Gets the transparency of filled faces. * * A value of ````0.0```` indicates fully transparent, ````1.0```` is fully opaque. * * Default is ````0.2````. * * @type {Number} */ get fillAlpha() { return this._state.fillAlpha; } /** * Sets if edges are visible. * * Default is ````true````. * * @type {Boolean} */ set edges(value) { value = value !== false; if (this._state.edges === value) { return; } this._state.edges = value; this.glRedraw(); } /** * Gets if edges are visible. * * Default is ````true````. * * @type {Boolean} */ get edges() { return this._state.edges; } /** * Sets the RGB color of edges. * * Default is ```` [0.2, 0.2, 0.2]````. * * @type {Number[]} */ set edgeColor(value) { let edgeColor = this._state.edgeColor; if (!edgeColor) { edgeColor = this._state.edgeColor = new Float32Array(3); } else if (value && edgeColor[0] === value[0] && edgeColor[1] === value[1] && edgeColor[2] === value[2]) { return; } if (value) { edgeColor[0] = value[0]; edgeColor[1] = value[1]; edgeColor[2] = value[2]; } else { edgeColor[0] = 0.2; edgeColor[1] = 0.2; edgeColor[2] = 0.2; } this.glRedraw(); } /** * Gets the RGB color of edges. * * Default is ```` [0.2, 0.2, 0.2]````. * * @type {Number[]} */ get edgeColor() { return this._state.edgeColor; } /** * Sets the transparency of edges. * * A value of ````0.0```` indicates fully transparent, ````1.0```` is fully opaque. * * Default is ````0.2````. * * @type {Number} */ set edgeAlpha(value) { value = (value !== undefined && value !== null) ? value : 0.5; if (this._state.edgeAlpha === value) { return; } this._state.edgeAlpha = value; this.glRedraw(); } /** * Gets the transparency of edges. * * A value of ````0.0```` indicates fully transparent, ````1.0```` is fully opaque. * * Default is ````0.2````. * * @type {Number} */ get edgeAlpha() { return this._state.edgeAlpha; } /** * Sets edge width. * * This is not supported by WebGL implementations based on DirectX [2019]. * * Default value is ````1.0```` pixels. * * @type {Number} */ set edgeWidth(value) { this._state.edgeWidth = value || 1.0; this.glRedraw(); } /** * Gets edge width. * * This is not supported by WebGL implementations based on DirectX [2019]. * * Default value is ````1.0```` pixels. * * @type {Number} */ get edgeWidth() { return this._state.edgeWidth; } /** * Sets whether to render backfaces when {@link EmphasisMaterial#fill} is ````true````. * * Default is ````false````. * * @type {Boolean} */ set backfaces(value) { value = !!value; if (this._state.backfaces === value) { return; } this._state.backfaces = value; this.glRedraw(); } /** * Gets whether to render backfaces when {@link EmphasisMaterial#fill} is ````true````. * * Default is ````true````. * * @type {Boolean} */ get backfaces() { return this._state.backfaces; } /** * Sets whether to render emphasized objects over the top of other objects, as if they were "glowing through". * * Default is ````true````. * * Note: updating this property will not affect the appearance of objects that are already emphasized. * * @type {Boolean} */ set glowThrough(value) { value = (value !== false); if (this._state.glowThrough === value) { return; } this._state.glowThrough = value; this.glRedraw(); } /** * Sets whether to render emphasized objects over the top of other objects, as if they were "glowing through". * * Default is ````true````. * * @type {Boolean} */ get glowThrough() { return this._state.glowThrough; } /** * @deprecated * Selects a preset EmphasisMaterial configuration. * * Default value is "default". * * @type {String} */ set preset(value) { console.warn("EmphasisMaterial.preset is deprecated and will be removed in future versions."); value = value || "default"; if (this._preset === value) { return; } const preset = PRESETS$3[value]; if (!preset) { this.error("unsupported preset: '" + value + "' - supported values are " + Object.keys(PRESETS$3).join(", ")); return; } this.fill = preset.fill; this.fillColor = preset.fillColor; this.fillAlpha = preset.fillAlpha; this.edges = preset.edges; this.edgeColor = preset.edgeColor; this.edgeAlpha = preset.edgeAlpha; this.edgeWidth = preset.edgeWidth; this.glowThrough = preset.glowThrough; this._preset = value; } /** * @deprecated * Gets the current preset EmphasisMaterial configuration. * * Default value is "default". * * @type {String} */ get preset() { console.warn("EmphasisMaterial.preset is deprecated and will be removed in future versions."); return this._preset; } /** * Destroys this EmphasisMaterial. */ destroy() { super.destroy(); this._state.destroy(); } } const PRESETS$2 = { "default": { edgeColor: [0.0, 0.0, 0.0], edgeAlpha: 1.0, edgeWidth: 1 }, "defaultWhiteBG": { edgeColor: [0.2, 0.2, 0.2], edgeAlpha: 1.0, edgeWidth: 1 }, "defaultLightBG": { edgeColor: [0.2, 0.2, 0.2], edgeAlpha: 1.0, edgeWidth: 1 }, "defaultDarkBG": { edgeColor: [0.5, 0.5, 0.5], edgeAlpha: 1.0, edgeWidth: 1 } }; /** * @desc Configures the appearance of {@link Entity}s when their edges are emphasised. * * * Emphasise edges of an {@link Entity} by setting {@link Entity#edges} ````true````. * * When {@link Entity}s are within the subtree of a root {@link Entity}, then setting {@link Entity#edges} on the root * will collectively set that property on all sub-{@link Entity}s. * * EdgeMaterial provides several presets. Select a preset by setting {@link EdgeMaterial#preset} to the ID of a preset in {@link EdgeMaterial#presets}. * * By default, a {@link Mesh} uses the default EdgeMaterial in {@link Scene#edgeMaterial}, but you can assign each {@link Mesh#edgeMaterial} to a custom EdgeMaterial if required. * * ## Usage * * In the example below, we'll create a {@link Mesh} with its own EdgeMaterial and set {@link Mesh#edges} ````true```` to emphasise its edges. * * Recall that {@link Mesh} is a concrete subtype of the abstract {@link Entity} base class. * * [[Run this example](/examples/index.html#materials_EdgeMaterial)] * * ````javascript * import {Viewer, Mesh, buildSphereGeometry, * ReadableGeometry, PhongMaterial, EdgeMaterial} from "xeokit-sdk.es.js"; * * const viewer = new Viewer({ * canvasId: "myCanvas", * transparent: true * }); * * viewer.scene.camera.eye = [0, 0, 5]; * viewer.scene.camera.look = [0, 0, 0]; * viewer.scene.camera.up = [0, 1, 0]; * * new Mesh(viewer.scene, { * * geometry: new ReadableGeometry(viewer.scene, buildSphereGeometry({ * radius: 1.5, * heightSegments: 24, * widthSegments: 16, * edgeThreshold: 2 // Default is 10 * })), * * material: new PhongMaterial(viewer.scene, { * diffuse: [0.4, 0.4, 1.0], * ambient: [0.9, 0.3, 0.9], * shininess: 30, * alpha: 0.5, * alphaMode: "blend" * }), * * edgeMaterial: new EdgeMaterial(viewer.scene, { * edgeColor: [0.0, 0.0, 1.0] * edgeAlpha: 1.0, * edgeWidth: 2 * }), * * edges: true * }); * ```` * * Note the ````edgeThreshold```` configuration for the {@link ReadableGeometry} on our {@link Mesh}. EdgeMaterial configures * a wireframe representation of the {@link ReadableGeometry}, which will have inner edges (those edges between * adjacent co-planar triangles) removed for visual clarity. The ````edgeThreshold```` indicates that, for * this particular {@link ReadableGeometry}, an inner edge is one where the angle between the surface normals of adjacent triangles * is not greater than ````5```` degrees. That's set to ````2```` by default, but we can override it to tweak the effect * as needed for particular Geometries. * * Here's the example again, this time implicitly defaulting to the {@link Scene#edgeMaterial}. We'll also modify that EdgeMaterial * to customize the effect. * * ````javascript * new Mesh({ * geometry: new ReadableGeometry(viewer.scene, buildSphereGeometry({ * radius: 1.5, * heightSegments: 24, * widthSegments: 16, * edgeThreshold: 2 // Default is 10 * })), * material: new PhongMaterial(viewer.scene, { * diffuse: [0.2, 0.2, 1.0] * }), * edges: true * }); * * var edgeMaterial = viewer.scene.edgeMaterial; * * edgeMaterial.edgeColor = [0.2, 1.0, 0.2]; * edgeMaterial.edgeAlpha = 1.0; * edgeMaterial.edgeWidth = 2; * ```` * * ## Presets * * Let's switch the {@link Scene#edgeMaterial} to one of the presets in {@link EdgeMaterial#presets}: * * ````javascript * viewer.edgeMaterial.preset = EdgeMaterial.presets["sepia"]; * ```` * * We can also create an EdgeMaterial from a preset, while overriding properties of the preset as required: * * ````javascript * var myEdgeMaterial = new EdgeMaterial(viewer.scene, { * preset: "sepia", * edgeColor = [1.0, 0.5, 0.5] * }); * ```` */ class EdgeMaterial extends Material { /** @private */ get type() { return "EdgeMaterial"; } /** * Gets available EdgeMaterial presets. * * @type {Object} */ get presets() { return PRESETS$2; }; /** * @constructor * @param {Component} owner Owner component. When destroyed, the owner will destroy this component as well. * @param {*} [cfg] The EdgeMaterial configuration * @param {String} [cfg.id] Optional ID, unique among all components in the parent {@link Scene}, generated automatically when omitted. * @param {Number[]} [cfg.edgeColor=[0.2,0.2,0.2]] RGB edge color. * @param {Number} [cfg.edgeAlpha=1.0] Edge transparency. A value of ````0.0```` indicates fully transparent, ````1.0```` is fully opaque. * @param {Number} [cfg.edgeWidth=1] Edge width in pixels. * @param {String} [cfg.preset] Selects a preset EdgeMaterial configuration - see {@link EdgeMaterial#presets}. */ constructor(owner, cfg = {}) { super(owner, cfg); this._state = new RenderState({ type: "EdgeMaterial", edges: null, edgeColor: null, edgeAlpha: null, edgeWidth: null }); this._preset = "default"; if (cfg.preset) { // Apply preset then override with configs where provided this.preset = cfg.preset; if (cfg.edgeColor) { this.edgeColor = cfg.edgeColor; } if (cfg.edgeAlpha !== undefined) { this.edgeAlpha = cfg.edgeAlpha; } if (cfg.edgeWidth !== undefined) { this.edgeWidth = cfg.edgeWidth; } } else { this.edgeColor = cfg.edgeColor; this.edgeAlpha = cfg.edgeAlpha; this.edgeWidth = cfg.edgeWidth; } this.edges = (cfg.edges !== false); } /** * Sets if edges are visible. * * Default is ````true````. * * @type {Boolean} */ set edges(value) { value = value !== false; if (this._state.edges === value) { return; } this._state.edges = value; this.glRedraw(); } /** * Gets if edges are visible. * * Default is ````true````. * * @type {Boolean} */ get edges() { return this._state.edges; } /** * Sets RGB edge color. * * Default value is ````[0.2, 0.2, 0.2]````. * * @type {Number[]} */ set edgeColor(value) { let edgeColor = this._state.edgeColor; if (!edgeColor) { edgeColor = this._state.edgeColor = new Float32Array(3); } else if (value && edgeColor[0] === value[0] && edgeColor[1] === value[1] && edgeColor[2] === value[2]) { return; } if (value) { edgeColor[0] = value[0]; edgeColor[1] = value[1]; edgeColor[2] = value[2]; } else { edgeColor[0] = 0.2; edgeColor[1] = 0.2; edgeColor[2] = 0.2; } this.glRedraw(); } /** * Gets RGB edge color. * * Default value is ````[0.2, 0.2, 0.2]````. * * @type {Number[]} */ get edgeColor() { return this._state.edgeColor; } /** * Sets edge transparency. * * A value of ````0.0```` indicates fully transparent, ````1.0```` is fully opaque. * * Default value is ````1.0````. * * @type {Number} */ set edgeAlpha(value) { value = (value !== undefined && value !== null) ? value : 1.0; if (this._state.edgeAlpha === value) { return; } this._state.edgeAlpha = value; this.glRedraw(); } /** * Gets edge transparency. * * A value of ````0.0```` indicates fully transparent, ````1.0```` is fully opaque. * * Default value is ````1.0````. * * @type {Number} */ get edgeAlpha() { return this._state.edgeAlpha; } /** * Sets edge width. * * This is not supported by WebGL implementations based on DirectX [2019]. * * Default value is ````1.0```` pixels. * * @type {Number} */ set edgeWidth(value) { this._state.edgeWidth = value || 1.0; this.glRedraw(); } /** * Gets edge width. * * This is not supported by WebGL implementations based on DirectX [2019]. * * Default value is ````1.0```` pixels. * * @type {Number} */ get edgeWidth() { return this._state.edgeWidth; } /** * Selects a preset EdgeMaterial configuration. * * Default value is ````"default"````. * * @type {String} */ set preset(value) { value = value || "default"; if (this._preset === value) { return; } const preset = PRESETS$2[value]; if (!preset) { this.error("unsupported preset: '" + value + "' - supported values are " + Object.keys(PRESETS$2).join(", ")); return; } this.edgeColor = preset.edgeColor; this.edgeAlpha = preset.edgeAlpha; this.edgeWidth = preset.edgeWidth; this._preset = value; } /** * The current preset EdgeMaterial configuration. * * Default value is ````"default"````. * * @type {String} */ get preset() { return this._preset; } /** * Destroys this EdgeMaterial. */ destroy() { super.destroy(); this._state.destroy(); } } /** * @desc Configures Fresnel effects for {@link PhongMaterial}s. * * Fresnels are attached to {@link PhongMaterial}s, which are attached to {@link Mesh}es. * * ## Usage * * In the example below we'll create a {@link Mesh} with a {@link PhongMaterial} that applies a Fresnel to its alpha channel to give a glasss-like effect. * * [[Run this example](/examples/index.html#materials_Fresnel)] * * ````javascript * import {Viewer, Mesh, buildTorusGeometry, * ReadableGeometry, PhongMaterial, Texture, Fresnel} from "xeokit-sdk.es.js"; * * const viewer = new Viewer({ * canvasId: "myCanvas", * transparent: true * }); * * viewer.scene.camera.eye = [0, 0, 5]; * viewer.scene.camera.look = [0, 0, 0]; * viewer.scene.camera.up = [0, 1, 0]; * * new Mesh(viewer.scene, { * geometry: new ReadableGeometry(viewer.scene, buildTorusGeometry({ * center: [0, 0, 0], * radius: 1.5, * tube: 0.5, * radialSegments: 32, * tubeSegments: 24, * arc: Math.PI * 2.0 * }), * material: new PhongMaterial(viewer.scene, { * alpha: 0.9, * alphaMode: "blend", * ambient: [0.0, 0.0, 0.0], * shininess: 30, * diffuseMap: new Texture(viewer.scene, { * src: "textures/diffuse/uvGrid2.jpg" * }), * alphaFresnel: new Fresnel(viewer.scene, { v edgeBias: 0.2, * centerBias: 0.8, * edgeColor: [1.0, 1.0, 1.0], * centerColor: [0.0, 0.0, 0.0], * power: 2 * }) * }) * }); * ```` */ class Fresnel extends Component { /** * JavaScript class name for this Component. * * @type {String} */ get type() { return "Fresnel"; } /** * @constructor * @param {Component} owner Owner component. When destroyed, the owner will destroy this Fresnel as well. * @param {*} [cfg] Configs * @param {String} [cfg.id] Optional ID, unique among all components in the parent scene, generated automatically when omitted. * @param {Number[]} [cfg.edgeColor=[ 0.0, 0.0, 0.0 ]] Color used on edges. * @param {Number[]} [cfg.centerColor=[ 1.0, 1.0, 1.0 ]] Color used on center. * @param {Number} [cfg.edgeBias=0] Bias at the edge. * @param {Number} [cfg.centerBias=1] Bias at the center. * @param {Number} [cfg.power=0] The power. */ constructor(owner, cfg = {}) { super(owner, cfg); this._state = new RenderState({ edgeColor: math.vec3([0, 0, 0]), centerColor: math.vec3([1, 1, 1]), edgeBias: 0, centerBias: 1, power: 1 }); this.edgeColor = cfg.edgeColor; this.centerColor = cfg.centerColor; this.edgeBias = cfg.edgeBias; this.centerBias = cfg.centerBias; this.power = cfg.power; } /** * Sets the Fresnel's edge color. * * Default value is ````[0.0, 0.0, 0.0]````. * * @type {Number[]} */ set edgeColor(value) { this._state.edgeColor.set(value || [0.0, 0.0, 0.0]); this.glRedraw(); } /** * Gets the Fresnel's edge color. * * Default value is ````[0.0, 0.0, 0.0]````. * * @type {Number[]} */ get edgeColor() { return this._state.edgeColor; } /** * Sets the Fresnel's center color. * * Default value is ````[1.0, 1.0, 1.0]````. * * @type {Number[]} */ set centerColor(value) { this._state.centerColor.set(value || [1.0, 1.0, 1.0]); this.glRedraw(); } /** * Gets the Fresnel's center color. * * Default value is ````[1.0, 1.0, 1.0]````. * * @type {Number[]} */ get centerColor() { return this._state.centerColor; } /** * Sets the Fresnel's edge bias. * * Default value is ````0````. * * @type {Number} */ set edgeBias(value) { this._state.edgeBias = value || 0; this.glRedraw(); } /** * Gets the Fresnel's edge bias. * * Default value is ````0````. * * @type {Number} */ get edgeBias() { return this._state.edgeBias; } /** * Sets the Fresnel's center bias. * * Default value is ````1````. * * @type {Number} */ set centerBias(value) { this._state.centerBias = (value !== undefined && value !== null) ? value : 1; this.glRedraw(); } /** * Gets the Fresnel's center bias. * * Default value is ````1````. * * @type {Number} */ get centerBias() { return this._state.centerBias; } /** * Sets the Fresnel's power. * * Default value is ````1````. * * @type {Number} */ set power(value) { this._state.power = (value !== undefined && value !== null) ? value : 1; this.glRedraw(); } /** * Gets the Fresnel's power. * * Default value is ````1````. * * @type {Number} */ get power() { return this._state.power; } /** * Destroys this Fresnel. */ destroy() { super.destroy(); this._state.destroy(); } } /** * A plane-shaped 3D object containing a bitmap image. * * * Creates a 3D quad containing our bitmap, located and oriented using ````pos````, ````normal```` and ````up```` vectors. * * Registered by {@link Bitmap#id} in {@link Scene#bitmaps}. * * {@link BCFViewpointsPlugin} will save and load Bitmaps in BCF viewpoints. * * ## Usage * * In the example below, we'll load the Schependomlaan model, then use * an ````Bitmap```` to show a storey plan next to the model. * * [](https://xeokit.github.io/xeokit-sdk/examples/scenegraph/#ImagePlane_imageInSectionPlane) * * [[Run this example](https://xeokit.github.io/xeokit-sdk/examples/scenegraph/#ImagePlane_imageInSectionPlane)] * * ````javascript * import {Viewer, Bitmap, XKTLoaderPlugin} from "xeokit-sdk.es.js"; * * const viewer = new Viewer({ * canvasId: "myCanvas", * transparent: true * }); * * viewer.camera.eye = [-24.65, 21.69, 8.16]; * viewer.camera.look = [-14.62, 2.16, -1.38]; * viewer.camera.up = [0.59, 0.57, -0.56]; * * const xktLoader = new XKTLoaderPlugin(viewer); * * xktLoader.load({ * id: "myModel", * src: "./models/xkt/Schependomlaan.xkt", * edges: true * }); * * new Bitmap(viewer.scene, { * src: "./images/schependomlaanPlanView.png", * visible: true, // Show the Bitmap * height: 24.0, // Height of Bitmap * pos: [-15, 0, -10], // World-space position of Bitmap's center * normal: [0, -1, 0], // Vector perpendicular to Bitmap * up: [0, 0, 1], // Direction of Bitmap "up" * collidable: false, // Bitmap does not contribute to Scene boundary * clippable: true, // Bitmap can be clipped by SectionPlanes * pickable: true // Allow the ground plane to be picked * }); * ```` */ class Bitmap extends Component { /** * Creates a new Bitmap. * * Registers the Bitmap in {@link Scene#bitmaps}; causes Scene to fire a "bitmapCreated" event. * * @constructor * @param {Component} [owner] Owner component. When destroyed, the owner will destroy this ````Bitmap```` as well. * @param {*} [cfg] ````Bitmap```` configuration * @param {String} [cfg.id] Optional ID, unique among all components in the parent {@link Scene}, generated automatically when omitted. * @param {Boolean} [cfg.visible=true] Indicates whether or not this ````Bitmap```` is visible. * @param {Number[]} [cfg.pos=[0,0,0]] World-space position of the ````Bitmap````. * @param {Number[]} [cfg.normal=[0,0,1]] Normal vector indicating the direction the ````Bitmap```` faces. * @param {Number[]} [cfg.up=[0,1,0]] Direction of "up" for the ````Bitmap````. * @param {Number[]} [cfg.height=1] World-space height of the ````Bitmap````. * @param {Number[]} [cfg.matrix=[1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1]] Modelling transform matrix for the ````Bitmap````. Overrides the ````position````, ````height```, ````rotation```` and ````normal```` parameters. * @param {Boolean} [cfg.collidable=true] Indicates if the ````Bitmap```` is initially included in boundary calculations. * @param {Boolean} [cfg.clippable=true] Indicates if the ````Bitmap```` is initially clippable. * @param {Boolean} [cfg.pickable=true] Indicates if the ````Bitmap```` is initially pickable. * @param {Number} [cfg.opacity=1.0] ````Bitmap````'s initial opacity factor, multiplies by the rendered fragment alpha. * @param {String} [cfg.src] URL of image. Accepted file types are PNG and JPEG. * @param {HTMLImageElement} [cfg.image] An ````HTMLImageElement```` to source the image from. Overrides ````src````. * @param {String} [cfg.imageData] Image data as a base64 encoded string. * @param {String} [cfg.type="jpg"] Image MIME type. Accepted values are "jpg" and "png". Default is "jpg". Normally only needed with ````image```` or ````imageData````. Automatically inferred from file extension of ````src````, if the file has a recognized extension. */ constructor(owner, cfg = {}) { super(owner, cfg); this._type = cfg.type || (cfg.src ? cfg.src.split('.').pop() : null) || "jpg"; this._pos = math.vec3(cfg.pos || [0, 0, 0]); this._up = math.vec3(cfg.up || [0, 1, 0]); this._normal = math.vec3(cfg.normal || [0, 0, 1]); this._height = cfg.height || 1.0; this._origin = math.vec3(); this._rtcPos = math.vec3(); this._imageSize = math.vec2(); this._texture = new Texture(this, { flipY: true }); this._image = new Image(); if (this._type !== "jpg" && this._type !== "png") { this.error(`Unsupported type - defaulting to "jpg"`); this._type = "jpg"; } this._node = new Node$1(this, { matrix: math.inverseMat4( math.lookAtMat4v(this._pos, math.subVec3(this._pos, this._normal, math.mat4()), this._up, math.mat4())), children: [ this._bitmapMesh = new Mesh(this, { scale: [1, 1, 1], rotation: [-90, 0, 0], collidable: cfg.collidable, pickable: cfg.pickable, opacity: cfg.opacity, clippable: cfg.clippable, geometry: new ReadableGeometry(this, buildPlaneGeometry({ center: [0, 0, 0], xSize: 1, zSize: 1, xSegments: 2, zSegments: 2 })), material: new PhongMaterial(this, { diffuse: [0, 0, 0], ambient: [0, 0, 0], specular: [0, 0, 0], diffuseMap: this._texture, emissiveMap: this._texture, backfaces: true }) }) ] }); if (cfg.image) { this.image = cfg.image; } else if (cfg.src) { this.src = cfg.src; } else if (cfg.imageData) { this.imageData = cfg.imageData; } this.scene._bitmapCreated(this); } /** * Sets if this ````Bitmap```` is visible or not. * * Default value is ````true````. * * @param {Boolean} visible Set ````true```` to make this ````Bitmap```` visible. */ set visible(visible) { this._bitmapMesh.visible = visible; } /** * Gets if this ````Bitmap```` is visible or not. * * Default value is ````true````. * * @returns {Boolean} Returns ````true```` if visible. */ get visible() { return this._bitmapMesh.visible; } /** * Sets an ````HTMLImageElement```` to source the image from. * * Sets {@link Texture#src} null. * * You may also need to set {@link Bitmap#type}, if you want to read the image data with {@link Bitmap#imageData}. * * @type {HTMLImageElement} */ set image(image) { this._image = image; if (this._image) { this._texture.image = this._image; this._imageSize[0] = this._image.width; this._imageSize[1] = this._image.height; this._updateBitmapMeshScale(); } } /** * Gets the ````HTMLImageElement```` the ````Bitmap````'s image is sourced from, if set. * * Returns null if not set. * * @type {HTMLImageElement} */ get image() { return this._image; } /** * Sets an image file path that the ````Bitmap````'s image is sourced from. * * If the file extension is a recognized MIME type, also sets {@link Bitmap#type} to that MIME type. * * Accepted file types are PNG and JPEG. * * @type {String} */ set src(src) { if (src) { this._image.onload = () => { this._texture.image = this._image; this._imageSize[0] = this._image.width; this._imageSize[1] = this._image.height; this._updateBitmapMeshScale(); }; this._image.src = src; const ext = src.split('.').pop(); switch (ext) { case "jpeg": case "jpg": this._type = "jpg"; break; case "png": this._type = "png"; } } } /** * Gets the image file path that the ````Bitmap````'s image is sourced from, if set. * * Returns null if not set. * * @type {String} */ get src() { return this._image.src; } /** * Sets an image file path that the ````Bitmap````'s image is sourced from. * * Accepted file types are PNG and JPEG. * * Sets {@link Texture#image} null. * * You may also need to set {@link Bitmap#type}, if you want to read the image data with {@link Bitmap#imageData}. * * @type {String} */ set imageData(imageData) { this._image.onload = () => { this._texture.image = image; this._imageSize[0] = image.width; this._imageSize[1] = image.height; this._updateBitmapMeshScale(); }; this._image.src = imageData; } /** * Gets the image file path that the ````Bitmap````'s image is sourced from, if set. * * Returns null if not set. * * @type {String} */ get imageData() { const canvas = document.createElement('canvas'); const context = canvas.getContext('2d'); canvas.width = this._image.width; canvas.height = this._image.height; context.drawImage(this._image, 0, 0); return canvas.toDataURL(this._type === "jpg" ? 'image/jpeg' : 'image/png'); } /** * Sets the MIME type of this Bitmap. * * This is used by ````Bitmap```` when getting image data with {@link Bitmap#imageData}. * * Supported values are "jpg" and "png", * * Default is "jpg". * * @type {String} */ set type(type) { type = type || "jpg"; if (type !== "png" || type !== "jpg") { this.error("Unsupported value for `type` - supported types are `jpg` and `png` - defaulting to `jpg`"); type = "jpg"; } this._type = type; } /** * Gets the MIME type of this Bitmap. * * @type {String} */ get type() { return this._type; } /** * Gets the World-space position of this ````Bitmap````. * * Default value is ````[0, 0, 0]````. * * @returns {Number[]} Current position. */ get pos() { return this._pos; } /** * Gets the direction of the normal vector that is perpendicular to this ````Bitmap````. * * @returns {Number[]} value Current normal direction. */ get normal() { return this._normal; } /** * Gets the "up" direction of this ````Bitmap````. * * @returns {Number[]} value Current "up" direction. */ get up() { return this._up; } /** * Sets the World-space height of the ````Bitmap````. * * Default value is ````1.0````. * * @param {Number} height New World-space height of the ````Bitmap````. */ set height(height) { this._height = (height === undefined || height === null) ? 1.0 : height; if (this._image) { this._updateBitmapMeshScale(); } } /** * Gets the World-space height of the ````Bitmap````. * * Returns {Number} World-space height of the ````Bitmap````. */ get height() { return this._height; } /** * Sets if this ````Bitmap```` is included in boundary calculations. * * Default is ````true````. * * @type {Boolean} */ set collidable(value) { this._bitmapMesh.collidable = (value !== false); } /** * Gets if this ````Bitmap```` is included in boundary calculations. * * Default is ````true````. * * @type {Boolean} */ get collidable() { return this._bitmapMesh.collidable; } /** * Sets if this ````Bitmap```` is clippable. * * Clipping is done by the {@link SectionPlane}s in {@link Scene#sectionPlanes}. * * Default is ````true````. * * @type {Boolean} */ set clippable(value) { this._bitmapMesh.clippable = (value !== false); } /** * Gets if this ````Bitmap```` is clippable. * * Clipping is done by the {@link SectionPlane}s in {@link Scene#sectionPlanes}. * * Default is ````true````. * * @type {Boolean} */ get clippable() { return this._bitmapMesh.clippable; } /** * Sets if this ````Bitmap```` is pickable. * * Default is ````true````. * * @type {Boolean} */ set pickable(value) { this._bitmapMesh.pickable = (value !== false); } /** * Gets if this ````Bitmap```` is pickable. * * Default is ````true````. * * @type {Boolean} */ get pickable() { return this._bitmapMesh.pickable; } /** * Sets the opacity factor for this ````Bitmap````. * * This is a factor in range ````[0..1]```` which multiplies by the rendered fragment alphas. * * @type {Number} */ set opacity(opacity) { this._bitmapMesh.opacity = opacity; } /** * Gets this ````Bitmap````'s opacity factor. * * This is a factor in range ````[0..1]```` which multiplies by the rendered fragment alphas. * * @type {Number} */ get opacity() { return this._bitmapMesh.opacity; } /** * Destroys this ````Bitmap````. * * Removes the ```Bitmap```` from {@link Scene#bitmaps}; causes Scene to fire a "bitmapDestroyed" event. */ destroy() { super.destroy(); this.scene._bitmapDestroyed(this); } _updateBitmapMeshScale() { const aspect = this._imageSize[1] / this._imageSize[0]; this._bitmapMesh.scale = [this._height / aspect, 1.0, this._height]; } } /** * Uses edge adjacency counts to identify if the given triangle mesh can be rendered with backface culling enabled. * * If all edges are connected to exactly two triangles, then the mesh will likely be a closed solid, and we can safely * render it with backface culling enabled. * * Otherwise, the mesh is a surface, and we must render it with backface culling disabled. * * @private */ const isTriangleMeshSolid = (indices, positions) => { const vertexIndexMapping = []; let edges = []; function compareIndexPositions(a, b) { let posA, posB; for (let i = 0; i < 3; i++) { posA = positions [a * 3 + i]; posB = positions [b * 3 + i]; if (posA !== posB) { return posB - posA; } } return 0; } // Group together indices corresponding to same position coordinates let newIndices = indices.slice().sort(compareIndexPositions); // Calculate the mapping: // - from original index in indices array // - to indices-for-unique-positions let uniqueVertexIndex = null; for (let i = 0, len = newIndices.length; i < len; i++) { if (i === 0 || 0 !== compareIndexPositions( newIndices[i], newIndices[i - 1], )) { // different position uniqueVertexIndex = newIndices [i]; } vertexIndexMapping [ newIndices[i] ] = uniqueVertexIndex; } // Generate the list of edges for (let i = 0, len = indices.length; i < len; i += 3) { const a = vertexIndexMapping[indices[i]]; const b = vertexIndexMapping[indices[i + 1]]; const c = vertexIndexMapping[indices[i + 2]]; let a2 = a; let b2 = b; let c2 = c; if (a > b && a > c) { if (b > c) { a2 = a; b2 = b; c2 = c; } else { a2 = a; b2 = c; c2 = b; } } else if (b > a && b > c) { if (a > c) { a2 = b; b2 = a; c2 = c; } else { a2 = b; b2 = c; c2 = a; } } else if (c > a && c > b) { if (a > b) { a2 = c; b2 = a; c2 = b; } else { a2 = c; b2 = b; c2 = a; } } edges[i + 0] = [ a2, b2 ]; edges[i + 1] = [ b2, c2 ]; if (a2 > c2) { const temp = c2; c2 = a2; a2 = temp; } edges[i + 2] = [ c2, a2 ]; } // Group semantically equivalent edgdes together function compareEdges(e1, e2) { let a, b; for (let i = 0; i < 2; i++) { a = e1[i]; b = e2[i]; if (b !== a) { return b - a; } } return 0; } edges = edges.slice(0, indices.length); edges.sort(compareEdges); // Make sure each edge is used exactly twice let sameEdgeCount = 0; for (let i = 0; i < edges.length; i++) { if (i === 0 || 0 !== compareEdges( edges[i], edges[i - 1] )) { // different edge if (0 !== i && sameEdgeCount !== 2) { return false; } sameEdgeCount = 1; } else { // same edge sameEdgeCount++; } } if (edges.length > 0 && sameEdgeCount !== 2) { return false; } // Each edge is used exactly twice, this is a // watertight surface and hence a solid geometry. return true; }; const v1$1 = math.vec3(); const v2$1 = math.vec3(); const v3$1 = math.vec3(); /** * Calculates the volume of triangle meshes. */ class MeshVolume { constructor() { this.vertices = []; this.indices = []; this.reset(); } /** * Resets, ready to add vertices and indices for a new mesh. */ reset() { this.lenVertices = 0; this.lenIndices = 0; this.primitive = null; } setPrimitive(primitive) { this.primitive = primitive; } /** * Adds a vertex. * @param vertex */ addVertex(vertex) { this.vertices[this.lenVertices++] = vertex[0]; this.vertices[this.lenVertices++] = vertex[1]; this.vertices[this.lenVertices++] = vertex[2]; } /** * Adds an index. * @param index */ addIndex(index) { this.indices[this.lenIndices++] = index; } /** * Gets the volume of the mesh. * * The mesh must be a closed solid. * * @returns {number} The volume of the mesh, or -1 if the mesh is not solid, in which case volume cannot be determined. */ get volume() { const vertices = this.vertices; const indices = this.indices; if (this.primitive !== "solid" && this.primitive !== "surface" && this.primitive !== "triangles") { return -1; } if (this.primitive !== "solid" && !isTriangleMeshSolid(indices, vertices)) { return -1; } let volume = 0; for (let i = 0; i < this.lenIndices; i += 3) { const index1 = indices[i] * 3; const index2 = indices[i + 1] * 3; const index3 = indices[i + 2] * 3; v1$1[0] = vertices[index1]; v1$1[1] = vertices[index1 + 1]; v1$1[2] = vertices[index1 + 2]; v2$1[0] = vertices[index2]; v2$1[1] = vertices[index2 + 1]; v2$1[2] = vertices[index2 + 2]; v3$1[0] = vertices[index3]; v3$1[1] = vertices[index3 + 1]; v3$1[2] = vertices[index3 + 2]; math.cross3Vec3(v1$1, v2$1, v1$1); volume += math.dotVec3(v1$1, v3$1); } return volume / 6; } } /** * Singleton instance of {@link MeshVolume}. * @type {MeshVolume} */ const meshVolume = new MeshVolume(); const v1 = math.vec3(); const v2 = math.vec3(); const v3 = math.vec3(); const v4 = math.vec3(); const v5 = math.vec3(); const v6 = math.vec3(); /** * Calculates the surface area of triangle meshes. */ class MeshSurfaceArea { constructor() { this.vertices = []; this.indices = []; this.reset(); } /** * Resets, ready to add vertices and indices for a new mesh. */ reset() { this.lenVertices = 0; this.lenIndices = 0; } /** * Adds a vertex. * @param vertex */ addVertex(vertex) { this.vertices[this.lenVertices++] = vertex[0]; this.vertices[this.lenVertices++] = vertex[1]; this.vertices[this.lenVertices++] = vertex[2]; } /** * Adds an index. * @param index */ addIndex(index) { this.indices[this.lenIndices++] = index; } /** * Gets the surface area of the mesh. * * @returns {number} */ get surfaceArea() { const vertices = this.vertices; const indices = this.indices; let surfaceArea = 0; for (let i = 0; i < this.lenIndices; i += 3) { const index1 = indices[i] * 3; const index2 = indices[i + 1] * 3; const index3 = indices[i + 2] * 3; v1[0] = vertices[index1]; v1[1] = vertices[index1 + 1]; v1[2] = vertices[index1 + 2]; v2[0] = vertices[index2]; v2[1] = vertices[index2 + 1]; v2[2] = vertices[index2 + 2]; v3[0] = vertices[index3]; v3[1] = vertices[index3 + 1]; v3[2] = vertices[index3 + 2]; math.subVec3(v1, v2, v4); math.subVec3(v3, v2, v5); surfaceArea += math.lenVec3(math.cross3Vec3(v4, v5, v6)) * 0.5; } return surfaceArea; } } /** * Singleton instance of {@link MeshSurfaceArea}. * @type {MeshSurfaceArea} */ const meshSurfaceArea = new MeshSurfaceArea(); const tempOBB3$1 = math.OBB3(); const tempOBB3b = math.OBB3(); const tempOBB3c = math.OBB3(); /** * A mesh within a {@link SceneModel}. * * * Created with {@link SceneModel#createMesh} * * Belongs to exactly one {@link SceneModelEntity} * * Stored by ID in {@link SceneModel#meshes} * * Referenced by {@link SceneModelEntity#meshes} * * Can have a {@link SceneModelTransform} to dynamically scale, rotate and translate it. */ class SceneModelMesh { /** * @private */ constructor(model, id, color, opacity, transform, textureSet, layer = null, portionId = 0) { /** * The {@link SceneModel} that owns this SceneModelMesh. * * @type {SceneModel} */ this.model = model; /** * The {@link SceneModelEntity} that owns this SceneModelMesh. * * @type {SceneModelEntity} */ this.object = null; /** * @private */ this.parent = null; /** * The {@link SceneModelTransform} that transforms this SceneModelMesh. * * * This only exists when the SceneModelMesh is instancing its geometry. * * These are created with {@link SceneModel#createTransform} * * Each of these is also registered in {@link SceneModel#transforms}. * * @type {SceneModelTransform} */ this.transform = transform; /** * The {@link SceneModelTextureSet} that optionally textures this SceneModelMesh. * * * This only exists when the SceneModelMesh has texture. * * These are created with {@link SceneModel#createTextureSet} * * Each of these is also registered in {@link SceneModel#textureSets}. * * @type {SceneModelTextureSet} */ this.textureSet = textureSet; this._matrixDirty = false; this._matrixUpdateScheduled = false; /** * Unique ID of this SceneModelMesh. * * The SceneModelMesh is registered against this ID in {@link SceneModel#meshes}. */ this.id = id; /** * @private */ this.obb = null; this._aabbLocal = null; this._aabbWorld = math.AABB3(); this._aabbWorldDirty = false; /** * @private */ this.layer = layer; /** * @private */ this.portionId = portionId; this._color = new Uint8Array([color[0], color[1], color[2], opacity]); // [0..255] this._colorize = new Uint8Array([color[0], color[1], color[2], opacity]); // [0..255] this._colorizing = false; this._transparent = (opacity < 255); /** * @private */ this.numTriangles = 0; /** * @private * @type {null} */ this.origin = null; // Set By SceneModel /** * The {@link SceneModelEntity} that owns this SceneModelMesh. * * @type {SceneModelEntity} */ this.entity = null; if (transform) { transform._addMesh(this); } this._volume = null; this._surfaceArea = null; } _sceneModelDirty() { this._aabbWorldDirty = true; this.layer.aabbDirty = true; } _transformDirty() { if (!this._matrixDirty && !this._matrixUpdateScheduled) { this.model._meshMatrixDirty(this); this._matrixDirty = true; this._matrixUpdateScheduled = true; } this._aabbWorldDirty = true; this.layer.aabbDirty = true; if (this.entity) { this.entity._transformDirty(); } } _updateMatrix() { if (this.transform && this._matrixDirty) { this.layer.setMatrix(this.portionId, this.transform.worldMatrix); } this._matrixDirty = false; this._matrixUpdateScheduled = false; } _finalize(entityFlags) { this.layer.initFlags(this.portionId, entityFlags, this._transparent); } _finalize2() { if (this.layer.flushInitFlags) { this.layer.flushInitFlags(); } } _setVisible(entityFlags) { this.layer.setVisible(this.portionId, entityFlags, this._transparent); } _setColor(color) { this._color[0] = color[0]; this._color[1] = color[1]; this._color[2] = color[2]; if (!this._colorizing) { this.layer.setColor(this.portionId, this._color, false); } } _setColorize(colorize) { const setOpacity = false; if (colorize) { this._colorize[0] = colorize[0]; this._colorize[1] = colorize[1]; this._colorize[2] = colorize[2]; this.layer.setColor(this.portionId, this._colorize, setOpacity); this._colorizing = true; } else { this.layer.setColor(this.portionId, this._color, setOpacity); this._colorizing = false; } } _setOpacity(opacity, entityFlags) { const newTransparent = (opacity < 255); const lastTransparent = this._transparent; const changingTransparency = (lastTransparent !== newTransparent); this._color[3] = opacity; this._colorize[3] = opacity; this._transparent = newTransparent; if (this._colorizing) { this.layer.setColor(this.portionId, this._colorize); } else { this.layer.setColor(this.portionId, this._color); } if (changingTransparency) { this.layer.setTransparent(this.portionId, entityFlags, newTransparent); } } _setOffset(offset) { this.layer.setOffset(this.portionId, offset); } _setHighlighted(entityFlags) { this.layer.setHighlighted(this.portionId, entityFlags, this._transparent); } _setXRayed(entityFlags) { this.layer.setXRayed(this.portionId, entityFlags, this._transparent); } _setSelected(entityFlags) { this.layer.setSelected(this.portionId, entityFlags, this._transparent); } _setEdges(entityFlags) { this.layer.setEdges(this.portionId, entityFlags, this._transparent); } _setClippable(entityFlags) { this.layer.setClippable(this.portionId, entityFlags, this._transparent); } _setCollidable(entityFlags) { this.layer.setCollidable(this.portionId, entityFlags); } _setPickable(flags) { this.layer.setPickable(this.portionId, flags, this._transparent); } _setCulled(flags) { this.layer.setCulled(this.portionId, flags, this._transparent); } /** * @private */ canPickTriangle() { return false; } /** * @private */ drawPickTriangles(renderFlags, frameCtx) { // NOP } /** * @private */ pickTriangleSurface(pickResult) { // NOP } /** * @private */ precisionRayPickSurface(worldRayOrigin, worldRayDir, worldSurfacePos, worldSurfaceNormal) { return this.layer.precisionRayPickSurface ? this.layer.precisionRayPickSurface(this.portionId, worldRayOrigin, worldRayDir, worldSurfacePos, worldSurfaceNormal) : false; } /** * @private */ canPickWorldPos() { return true; } /** * @private */ drawPickDepths(frameCtx) { this.model.drawPickDepths(frameCtx); } /** * @private */ drawPickNormals(frameCtx) { this.model.drawPickNormals(frameCtx); } /** * @private */ delegatePickedEntity() { return this.parent; } /** * @private */ getEachVertex(callback) { if (this.layer.getEachVertex) { this.layer.getEachVertex(this.portionId, callback); } } /** * @private */ getEachIndex(callback) { if (this.layer.getEachIndex ) { this.layer.getEachIndex(this.portionId, callback); } } isSolid() { return this.layer.solid; } /** * Returns the volume of this SceneModelMesh. * @returns {number} */ get volume() { if (this._volume !== null) { return this._volume; } switch (this.layer.primitive) { case "solid": case "surface": case "triangles": meshVolume.reset(); meshVolume.setPrimitive(this.layer.primitive); this.getEachVertex((vertex) =>{ meshVolume.addVertex(vertex); }); this.getEachIndex((index)=>{ meshVolume.addIndex(index); }); this._volume = meshVolume.volume; break; default: this._volume = 0; break; } return this._volume; } /** * Returns the surface area of this SceneModelMesh. * @returns {number} */ get surfaceArea() { if (this._surfaceArea !== null) { return this._surfaceArea; } switch (this.layer.primitive) { case "solid": case "surface": case "triangles": meshSurfaceArea.reset(); this.getEachVertex((vertex) =>{ meshSurfaceArea.addVertex(vertex); }); this.getEachIndex((index)=>{ meshSurfaceArea.addIndex(index); }); this._surfaceArea = meshSurfaceArea.surfaceArea; break; default: this._surfaceArea = 0; break; } return this._surfaceArea; } /** * @private */ set aabb(aabb) { // Called by SceneModel this._aabbLocal = aabb; if (this.origin) { this._aabbLocal = aabb.slice(0); const origin = this.origin; this._aabbLocal[0] += origin[0]; this._aabbLocal[1] += origin[1]; this._aabbLocal[2] += origin[2]; this._aabbLocal[3] += origin[0]; this._aabbLocal[4] += origin[1]; this._aabbLocal[5] += origin[2]; } } /** * @private */ get aabb() { // called by SceneModelEntity if (this._aabbWorldDirty) { math.AABB3ToOBB3(this._aabbLocal, tempOBB3$1); if (this.transform) { math.transformOBB3(this.transform.worldMatrix, tempOBB3$1, tempOBB3b); math.transformOBB3(this.model.worldMatrix, tempOBB3b, tempOBB3c); math.OBB3ToAABB3(tempOBB3c, this._aabbWorld); } else { math.transformOBB3(this.model.worldMatrix, tempOBB3$1, tempOBB3b); math.OBB3ToAABB3(tempOBB3b, this._aabbWorld); } this._aabbWorldDirty = false; } return this._aabbWorld; } /** * @private */ _destroy() { this.model.scene._renderer.putPickID(this.pickId); } } /** * Provides scratch memory for methods like TrianglesBatchingLayer setFlags() and setColors(), * so they don't need to allocate temporary arrays that need garbage collection. * * @private */ class ScratchMemory { constructor() { this._uint8Arrays = {}; this._float32Arrays = {}; } _clear() { this._uint8Arrays = {}; this._float32Arrays = {}; } getUInt8Array(len) { let uint8Array = this._uint8Arrays[len]; if (!uint8Array) { uint8Array = new Uint8Array(len); this._uint8Arrays[len] = uint8Array; } return uint8Array; } getFloat32Array(len) { let float32Array = this._float32Arrays[len]; if (!float32Array) { float32Array = new Float32Array(len); this._float32Arrays[len] = float32Array; } return float32Array; } } const batchingLayerScratchMemory = new ScratchMemory(); let countUsers = 0; /** * @private */ function getScratchMemory() { countUsers++; return batchingLayerScratchMemory; } /** * @private */ function putScratchMemory() { if (countUsers === 0) { return; } countUsers--; if (countUsers === 0) { batchingLayerScratchMemory._clear(); } } /** * @private * @type {{PICKABLE: number, CLIPPABLE: number, BACKFACES: number, VISIBLE: number, SELECTED: number, OUTLINED: number, CULLED: number, RECEIVE_SHADOW: number, COLLIDABLE: number, XRAYED: number, CAST_SHADOW: number, EDGES: number, HIGHLIGHTED: number}} */ const ENTITY_FLAGS = { VISIBLE: 1, CULLED: 1 << 2, PICKABLE: 1 << 3, CLIPPABLE: 1 << 4, COLLIDABLE: 1 << 5, CAST_SHADOW: 1 << 6, RECEIVE_SHADOW: 1 << 7, XRAYED: 1 << 8, HIGHLIGHTED: 1 << 9, SELECTED: 1 << 10, EDGES: 1 << 11, BACKFACES: 1 << 12, TRANSPARENT: 1 << 13 }; /** * @private */ const RENDER_PASSES = { // Skipped - suppress rendering NOT_RENDERED: 0, // Normal rendering - mutually exclusive modes COLOR_OPAQUE: 1, COLOR_TRANSPARENT: 2, // Emphasis silhouette rendering - mutually exclusive modes SILHOUETTE_HIGHLIGHTED: 3, SILHOUETTE_SELECTED: 4, SILHOUETTE_XRAYED: 5, // Edges rendering - mutually exclusive modes EDGES_COLOR_OPAQUE: 6, EDGES_COLOR_TRANSPARENT: 7, EDGES_HIGHLIGHTED: 8, EDGES_SELECTED: 9, EDGES_XRAYED: 10, // Picking PICK: 11 }; const defaultColor$2 = new Float32Array([1, 1, 1, 1]); const edgesDefaultColor = new Float32Array([0, 0, 0, 1]); const tempVec4 = math.vec4(); const tempVec3a$F = math.vec3(); const tempVec3c$w = math.vec3(); const tempMat4a$r = math.mat4(); /** * @private */ class VBORenderer { constructor(scene, withSAO = false, {instancing = false, edges = false, useAlphaCutoff = false} = {}) { this._scene = scene; this._withSAO = withSAO; this._instancing = instancing; this._edges = edges; this._useAlphaCutoff = useAlphaCutoff; this._hash = this._getHash(); /** * Matrices Uniform Block Buffer * * In shaders, matrices in the Matrices Uniform Block MUST be set in this order: * - worldMatrix * - viewMatrix * - projMatrix * - positionsDecodeMatrix * - worldNormalMatrix * - viewNormalMatrix */ this._matricesUniformBlockBufferBindingPoint = 0; this._matricesUniformBlockBuffer = this._scene.canvas.gl.createBuffer(); this._matricesUniformBlockBufferData = new Float32Array(4 * 4 * 6); // there is 6 mat4 /** * A Vertex Array Object by Layer */ this._vaoCache = new WeakMap(); this._allocate(); } /** * Should be overrided by subclasses if it does not only "depend" on section planes state. * @returns { string } */ _getHash() { return this._scene._sectionPlanesState.getHash(); } _buildShader() { return { vertex: this._buildVertexShader(), fragment: this._buildFragmentShader() }; } _buildVertexShader() { return [""]; } _buildFragmentShader() { return [""]; } _addMatricesUniformBlockLines(src, normals = false) { src.push("uniform Matrices {"); src.push(" mat4 worldMatrix;"); src.push(" mat4 viewMatrix;"); src.push(" mat4 projMatrix;"); src.push(" mat4 positionsDecodeMatrix;"); if (normals) { src.push(" mat4 worldNormalMatrix;"); src.push(" mat4 viewNormalMatrix;"); } src.push("};"); return src; } _addRemapClipPosLines(src, viewportSize = 1) { src.push("uniform vec2 drawingBufferSize;"); src.push("uniform vec2 pickClipPos;"); src.push("vec4 remapClipPos(vec4 clipPos) {"); src.push(" clipPos.xy /= clipPos.w;"); if (viewportSize === 1) { src.push(" clipPos.xy = (clipPos.xy - pickClipPos) * drawingBufferSize;"); } else { src.push(` clipPos.xy = (clipPos.xy - pickClipPos) * (drawingBufferSize / float(${viewportSize}));`); } src.push(" clipPos.xy *= clipPos.w;"); src.push(" return clipPos;"); src.push("}"); return src; } getValid() { return this._hash === this._getHash(); } setSectionPlanesStateUniforms(layer) { const scene = this._scene; const {gl} = scene.canvas; const {model, layerIndex} = layer; const numAllocatedSectionPlanes = scene._sectionPlanesState.getNumAllocatedSectionPlanes(); const numSectionPlanes = scene._sectionPlanesState.sectionPlanes.length; if (numAllocatedSectionPlanes > 0) { const sectionPlanes = scene._sectionPlanesState.sectionPlanes; const baseIndex = layerIndex * numSectionPlanes; const renderFlags = model.renderFlags; if (scene.crossSections) { gl.uniform4fv(this._uSliceColor, scene.crossSections.sliceColor); gl.uniform1f(this._uSliceThickness, scene.crossSections.sliceThickness); } for (let sectionPlaneIndex = 0; sectionPlaneIndex < numAllocatedSectionPlanes; sectionPlaneIndex++) { const sectionPlaneUniforms = this._uSectionPlanes[sectionPlaneIndex]; if (sectionPlaneUniforms) { if (sectionPlaneIndex < numSectionPlanes) { const active = renderFlags.sectionPlanesActivePerLayer[baseIndex + sectionPlaneIndex]; gl.uniform1i(sectionPlaneUniforms.active, active ? 1 : 0); if (active) { const sectionPlane = sectionPlanes[sectionPlaneIndex]; const origin = layer._state.origin; if (origin) { const rtcSectionPlanePos = getPlaneRTCPos(sectionPlane.dist, sectionPlane.dir, origin, tempVec3a$F, model.matrix); gl.uniform3fv(sectionPlaneUniforms.pos, rtcSectionPlanePos); } else { gl.uniform3fv(sectionPlaneUniforms.pos, sectionPlane.pos); } gl.uniform3fv(sectionPlaneUniforms.dir, sectionPlane.dir); } } else { gl.uniform1i(sectionPlaneUniforms.active, 0); } } } } } _allocate() { const scene = this._scene; const gl = scene.canvas.gl; const lightsState = scene._lightsState; this._program = new Program(gl, this._buildShader()); if (this._program.errors) { this.errors = this._program.errors; return; } const program = this._program; this._uRenderPass = program.getLocation("renderPass"); this._uColor = program.getLocation("color"); if (!this._uColor) { // some shader may have color as attribute, in this case the uniform must be renamed silhouetteColor this._uColor = program.getLocation("silhouetteColor"); } this._uUVDecodeMatrix = program.getLocation("uvDecodeMatrix"); this._uPickInvisible = program.getLocation("pickInvisible"); this._uGammaFactor = program.getLocation("gammaFactor"); gl.uniformBlockBinding( program.handle, gl.getUniformBlockIndex(program.handle, "Matrices"), this._matricesUniformBlockBufferBindingPoint ); this._uShadowViewMatrix = program.getLocation("shadowViewMatrix"); this._uShadowProjMatrix = program.getLocation("shadowProjMatrix"); if (scene.logarithmicDepthBufferEnabled) { this._uZFar = program.getLocation("zFar"); } this._uLightAmbient = program.getLocation("lightAmbient"); this._uLightColor = []; this._uLightDir = []; this._uLightPos = []; this._uLightAttenuation = []; // TODO add a gard to prevent light params if not affected by light ? const lights = lightsState.lights; let light; for (let i = 0, len = lights.length; i < len; i++) { light = lights[i]; switch (light.type) { case "dir": this._uLightColor[i] = program.getLocation("lightColor" + i); this._uLightPos[i] = null; this._uLightDir[i] = program.getLocation("lightDir" + i); break; case "point": this._uLightColor[i] = program.getLocation("lightColor" + i); this._uLightPos[i] = program.getLocation("lightPos" + i); this._uLightDir[i] = null; this._uLightAttenuation[i] = program.getLocation("lightAttenuation" + i); break; case "spot": this._uLightColor[i] = program.getLocation("lightColor" + i); this._uLightPos[i] = program.getLocation("lightPos" + i); this._uLightDir[i] = program.getLocation("lightDir" + i); this._uLightAttenuation[i] = program.getLocation("lightAttenuation" + i); break; } } if (lightsState.reflectionMaps.length > 0) { this._uReflectionMap = "reflectionMap"; } if (lightsState.lightMaps.length > 0) { this._uLightMap = "lightMap"; } this._uSectionPlanes = []; for (let i = 0, len = scene._sectionPlanesState.getNumAllocatedSectionPlanes(); i < len; i++) { this._uSectionPlanes.push({ active: program.getLocation("sectionPlaneActive" + i), pos: program.getLocation("sectionPlanePos" + i), dir: program.getLocation("sectionPlaneDir" + i) }); } this._aPosition = program.getAttribute("position"); this._aOffset = program.getAttribute("offset"); this._aNormal = program.getAttribute("normal"); this._aUV = program.getAttribute("uv"); this._aColor = program.getAttribute("color"); this._aMetallicRoughness = program.getAttribute("metallicRoughness"); this._aFlags = program.getAttribute("flags"); this._aPickColor = program.getAttribute("pickColor"); this._uPickZNear = program.getLocation("pickZNear"); this._uPickZFar = program.getLocation("pickZFar"); this._uPickClipPos = program.getLocation("pickClipPos"); this._uDrawingBufferSize = program.getLocation("drawingBufferSize"); this._uColorMap = "uColorMap"; this._uMetallicRoughMap = "uMetallicRoughMap"; this._uEmissiveMap = "uEmissiveMap"; this._uNormalMap = "uNormalMap"; this._uAOMap = "uAOMap"; if (this._instancing) { this._aModelMatrix = program.getAttribute("modelMatrix"); this._aModelMatrixCol0 = program.getAttribute("modelMatrixCol0"); this._aModelMatrixCol1 = program.getAttribute("modelMatrixCol1"); this._aModelMatrixCol2 = program.getAttribute("modelMatrixCol2"); this._aModelNormalMatrixCol0 = program.getAttribute("modelNormalMatrixCol0"); this._aModelNormalMatrixCol1 = program.getAttribute("modelNormalMatrixCol1"); this._aModelNormalMatrixCol2 = program.getAttribute("modelNormalMatrixCol2"); } if (this._withSAO) { this._uOcclusionTexture = "uOcclusionTexture"; this._uSAOParams = program.getLocation("uSAOParams"); } if (this._useAlphaCutoff) { this._alphaCutoffLocation = program.getLocation("materialAlphaCutoff"); } if (scene.logarithmicDepthBufferEnabled) { this._uLogDepthBufFC = program.getLocation("logDepthBufFC"); } if (scene.pointsMaterial._state.filterIntensity) { this._uIntensityRange = program.getLocation("intensityRange"); } this._uPointSize = program.getLocation("pointSize"); this._uNearPlaneHeight = program.getLocation("nearPlaneHeight"); if (scene.crossSections) { this._uSliceColor = program.getLocation("sliceColor"); this._uSliceThickness = program.getLocation("sliceThickness"); } } _bindProgram(frameCtx) { const scene = this._scene; const gl = scene.canvas.gl; const program = this._program; const lightsState = scene._lightsState; const lights = lightsState.lights; program.bind(); frameCtx.textureUnit = 0; if (this._uLightAmbient) { gl.uniform4fv(this._uLightAmbient, lightsState.getAmbientColorAndIntensity()); } if (this._uGammaFactor) { gl.uniform1f(this._uGammaFactor, scene.gammaFactor); } for (let i = 0, len = lights.length; i < len; i++) { const light = lights[i]; if (this._uLightColor[i]) { gl.uniform4f(this._uLightColor[i], light.color[0], light.color[1], light.color[2], light.intensity); } if (this._uLightPos[i]) { gl.uniform3fv(this._uLightPos[i], light.pos); if (this._uLightAttenuation[i]) { gl.uniform1f(this._uLightAttenuation[i], light.attenuation); } } if (this._uLightDir[i]) { gl.uniform3fv(this._uLightDir[i], light.dir); } } } _makeVAO(state) { const gl = this._scene.canvas.gl; const vao = gl.createVertexArray(); gl.bindVertexArray(vao); if (this._instancing) { this._aModelMatrixCol0.bindArrayBuffer(state.modelMatrixCol0Buf); this._aModelMatrixCol1.bindArrayBuffer(state.modelMatrixCol1Buf); this._aModelMatrixCol2.bindArrayBuffer(state.modelMatrixCol2Buf); gl.vertexAttribDivisor(this._aModelMatrixCol0.location, 1); gl.vertexAttribDivisor(this._aModelMatrixCol1.location, 1); gl.vertexAttribDivisor(this._aModelMatrixCol2.location, 1); if (this._aModelNormalMatrixCol0) { this._aModelNormalMatrixCol0.bindArrayBuffer(state.modelNormalMatrixCol0Buf); gl.vertexAttribDivisor(this._aModelNormalMatrixCol0.location, 1); } if (this._aModelNormalMatrixCol1) { this._aModelNormalMatrixCol1.bindArrayBuffer(state.modelNormalMatrixCol1Buf); gl.vertexAttribDivisor(this._aModelNormalMatrixCol1.location, 1); } if (this._aModelNormalMatrixCol2) { this._aModelNormalMatrixCol2.bindArrayBuffer(state.modelNormalMatrixCol2Buf); gl.vertexAttribDivisor(this._aModelNormalMatrixCol2.location, 1); } } this._aPosition.bindArrayBuffer(state.positionsBuf); if (this._aUV) { this._aUV.bindArrayBuffer(state.uvBuf); } if (this._aNormal) { this._aNormal.bindArrayBuffer(state.normalsBuf); } if (this._aMetallicRoughness) { this._aMetallicRoughness.bindArrayBuffer(state.metallicRoughnessBuf); if (this._instancing) { gl.vertexAttribDivisor(this._aMetallicRoughness.location, 1); } } if (this._aColor) { this._aColor.bindArrayBuffer(state.colorsBuf); if (this._instancing && state.colorsBuf) { gl.vertexAttribDivisor(this._aColor.location, 1); } } if (this._aFlags) { this._aFlags.bindArrayBuffer(state.flagsBuf); if (this._instancing) { gl.vertexAttribDivisor(this._aFlags.location, 1); } } if (this._aOffset) { this._aOffset.bindArrayBuffer(state.offsetsBuf); if (this._instancing) { gl.vertexAttribDivisor(this._aOffset.location, 1); } } if (this._aPickColor) { this._aPickColor.bindArrayBuffer(state.pickColorsBuf); if (this._instancing) { gl.vertexAttribDivisor(this._aPickColor.location, 1); } } if (this._instancing) { if (this._edges && state.edgeIndicesBuf) { state.edgeIndicesBuf.bind(); } else { if (state.indicesBuf) { state.indicesBuf.bind(); } } } else { if (this._edges && state.edgeIndicesBuf) { state.edgeIndicesBuf.bind(); } else { if (state.indicesBuf) { state.indicesBuf.bind(); } } } return vao; } drawLayer(frameCtx, layer, renderPass, {colorUniform = false, incrementDrawState = false} = {}) { const maxTextureUnits = WEBGL_INFO.MAX_TEXTURE_IMAGE_UNITS; const scene = this._scene; const gl = scene.canvas.gl; const {_state: state, model} = layer; const {textureSet, origin, positionsDecodeMatrix} = state; const lightsState = scene._lightsState; const pointsMaterial = scene.pointsMaterial; const {camera} = model.scene; const {viewNormalMatrix, project} = camera; const viewMatrix = frameCtx.pickViewMatrix || camera.viewMatrix; const {position, rotationMatrix, worldNormalMatrix} = model; if (!this._program) { this._allocate(); if (this.errors) { return; } } if (frameCtx.lastProgramId !== this._program.id) { frameCtx.lastProgramId = this._program.id; this._bindProgram(frameCtx); } if (this._vaoCache.has(layer)) { gl.bindVertexArray(this._vaoCache.get(layer)); } else { this._vaoCache.set(layer, this._makeVAO(state)); } let offset = 0; const mat4Size = 4 * 4; this._matricesUniformBlockBufferData.set(rotationMatrix, 0); const gotOrigin = (origin[0] !== 0 || origin[1] !== 0 || origin[2] !== 0); const gotPosition = (position[0] !== 0 || position[1] !== 0 || position[2] !== 0); if (gotOrigin || gotPosition) { const rtcOrigin = tempVec3a$F; if (gotOrigin) { const rotatedOrigin = math.transformPoint3(rotationMatrix, origin, tempVec3c$w); rtcOrigin[0] = rotatedOrigin[0]; rtcOrigin[1] = rotatedOrigin[1]; rtcOrigin[2] = rotatedOrigin[2]; } else { rtcOrigin[0] = 0; rtcOrigin[1] = 0; rtcOrigin[2] = 0; } rtcOrigin[0] += position[0]; rtcOrigin[1] += position[1]; rtcOrigin[2] += position[2]; this._matricesUniformBlockBufferData.set(createRTCViewMat(viewMatrix, rtcOrigin, tempMat4a$r), offset += mat4Size); } else { this._matricesUniformBlockBufferData.set(viewMatrix, offset += mat4Size); } this._matricesUniformBlockBufferData.set(frameCtx.pickProjMatrix || project.matrix, offset += mat4Size); this._matricesUniformBlockBufferData.set(positionsDecodeMatrix, offset += mat4Size); this._matricesUniformBlockBufferData.set(worldNormalMatrix, offset += mat4Size); this._matricesUniformBlockBufferData.set(viewNormalMatrix, offset += mat4Size); gl.bindBuffer(gl.UNIFORM_BUFFER, this._matricesUniformBlockBuffer); gl.bufferData(gl.UNIFORM_BUFFER, this._matricesUniformBlockBufferData, gl.DYNAMIC_DRAW); gl.bindBufferBase( gl.UNIFORM_BUFFER, this._matricesUniformBlockBufferBindingPoint, this._matricesUniformBlockBuffer); gl.uniform1i(this._uRenderPass, renderPass); this.setSectionPlanesStateUniforms(layer); if (scene.logarithmicDepthBufferEnabled) { if (this._uLogDepthBufFC) { const logDepthBufFC = 2.0 / (Math.log(frameCtx.pickZFar + 1.0) / Math.LN2); // TODO: Far from pick project matrix? gl.uniform1f(this._uLogDepthBufFC, logDepthBufFC); } if (this._uZFar) { gl.uniform1f(this._uZFar, scene.camera.project.far); } } if (this._uPickInvisible) { gl.uniform1i(this._uPickInvisible, frameCtx.pickInvisible); } if (this._uPickZNear) { gl.uniform1f(this._uPickZNear, frameCtx.pickZNear); } if (this._uPickZFar) { gl.uniform1f(this._uPickZFar, frameCtx.pickZFar); } if (this._uPickClipPos) { gl.uniform2fv(this._uPickClipPos, frameCtx.pickClipPos); } if (this._uDrawingBufferSize) { gl.uniform2f(this._uDrawingBufferSize, gl.drawingBufferWidth, gl.drawingBufferHeight); } if (this._uUVDecodeMatrix) { gl.uniformMatrix3fv(this._uUVDecodeMatrix, false, state.uvDecodeMatrix); } if (this._uIntensityRange && pointsMaterial.filterIntensity) { gl.uniform2f(this._uIntensityRange, pointsMaterial.minIntensity, pointsMaterial.maxIntensity); } if (this._uPointSize) { gl.uniform1f(this._uPointSize, pointsMaterial.pointSize); } if (this._uNearPlaneHeight) { const nearPlaneHeight = (scene.camera.projection === "ortho") ? 1.0 : (gl.drawingBufferHeight / (2 * Math.tan(0.5 * scene.camera.perspective.fov * Math.PI / 180.0))); gl.uniform1f(this._uNearPlaneHeight, nearPlaneHeight); } if (textureSet) { const { colorTexture, metallicRoughnessTexture, emissiveTexture, normalsTexture, occlusionTexture, } = textureSet; if (this._uColorMap && colorTexture) { this._program.bindTexture(this._uColorMap, colorTexture.texture, frameCtx.textureUnit); frameCtx.textureUnit = (frameCtx.textureUnit + 1) % maxTextureUnits; } if (this._uMetallicRoughMap && metallicRoughnessTexture) { this._program.bindTexture(this._uMetallicRoughMap, metallicRoughnessTexture.texture, frameCtx.textureUnit); frameCtx.textureUnit = (frameCtx.textureUnit + 1) % maxTextureUnits; } if (this._uEmissiveMap && emissiveTexture) { this._program.bindTexture(this._uEmissiveMap, emissiveTexture.texture, frameCtx.textureUnit); frameCtx.textureUnit = (frameCtx.textureUnit + 1) % maxTextureUnits; } if (this._uNormalMap && normalsTexture) { this._program.bindTexture(this._uNormalMap, normalsTexture.texture, frameCtx.textureUnit); frameCtx.textureUnit = (frameCtx.textureUnit + 1) % maxTextureUnits; } if (this._uAOMap && occlusionTexture) { this._program.bindTexture(this._uAOMap, occlusionTexture.texture, frameCtx.textureUnit); frameCtx.textureUnit = (frameCtx.textureUnit + 1) % maxTextureUnits; } } if (lightsState.reflectionMaps.length > 0 && lightsState.reflectionMaps[0].texture && this._uReflectionMap) { this._program.bindTexture(this._uReflectionMap, lightsState.reflectionMaps[0].texture, frameCtx.textureUnit); frameCtx.textureUnit = (frameCtx.textureUnit + 1) % maxTextureUnits; frameCtx.bindTexture++; } if (lightsState.lightMaps.length > 0 && lightsState.lightMaps[0].texture && this._uLightMap) { this._program.bindTexture(this._uLightMap, lightsState.lightMaps[0].texture, frameCtx.textureUnit); frameCtx.textureUnit = (frameCtx.textureUnit + 1) % maxTextureUnits; frameCtx.bindTexture++; } if (this._withSAO) { const sao = scene.sao; const saoEnabled = sao.possible; if (saoEnabled) { const viewportWidth = gl.drawingBufferWidth; const viewportHeight = gl.drawingBufferHeight; tempVec4[0] = viewportWidth; tempVec4[1] = viewportHeight; tempVec4[2] = sao.blendCutoff; tempVec4[3] = sao.blendFactor; gl.uniform4fv(this._uSAOParams, tempVec4); this._program.bindTexture(this._uOcclusionTexture, frameCtx.occlusionTexture, frameCtx.textureUnit); frameCtx.textureUnit = (frameCtx.textureUnit + 1) % maxTextureUnits; frameCtx.bindTexture++; } } if (this._useAlphaCutoff) { gl.uniform1f(this._alphaCutoffLocation, textureSet.alphaCutoff); } if (colorUniform) { const colorKey = this._edges ? "edgeColor" : "fillColor"; const alphaKey = this._edges ? "edgeAlpha" : "fillAlpha"; if (renderPass === RENDER_PASSES[`${this._edges ? "EDGES" : "SILHOUETTE"}_XRAYED`]) { const material = scene.xrayMaterial._state; const color = material[colorKey]; const alpha = material[alphaKey]; gl.uniform4f(this._uColor, color[0], color[1], color[2], alpha); } else if (renderPass === RENDER_PASSES[`${this._edges ? "EDGES" : "SILHOUETTE"}_HIGHLIGHTED`]) { const material = scene.highlightMaterial._state; const color = material[colorKey]; const alpha = material[alphaKey]; gl.uniform4f(this._uColor, color[0], color[1], color[2], alpha); } else if (renderPass === RENDER_PASSES[`${this._edges ? "EDGES" : "SILHOUETTE"}_SELECTED`]) { const material = scene.selectedMaterial._state; const color = material[colorKey]; const alpha = material[alphaKey]; gl.uniform4f(this._uColor, color[0], color[1], color[2], alpha); } else { gl.uniform4fv(this._uColor, this._edges ? edgesDefaultColor : defaultColor$2); } } this._draw({state, frameCtx, incrementDrawState}); gl.bindVertexArray(null); } webglContextRestored() { this._program = null; } destroy() { if (this._program) { this._program.destroy(); } this._program = null; stats.memory.programs--; } } /** * @private */ class TrianglesBatchingRenderer extends VBORenderer { constructor(scene, withSAO, {edges = false, useAlphaCutoff = false} = {}) { super(scene, withSAO, {instancing: false, edges, useAlphaCutoff}); } _draw(drawCfg) { const {gl} = this._scene.canvas; const { state, frameCtx, incrementDrawState } = drawCfg; if (this._edges) { if (state.edgeIndicesBuf) { gl.drawElements(gl.LINES, state.edgeIndicesBuf.numItems, state.edgeIndicesBuf.itemType, 0); } } else { const count = frameCtx.pickElementsCount || state.indicesBuf.numItems; const offset = frameCtx.pickElementsOffset ? frameCtx.pickElementsOffset * state.indicesBuf.itemByteSize : 0; gl.drawElements(gl.TRIANGLES, count, state.indicesBuf.itemType, offset); if (incrementDrawState) { frameCtx.drawElements++; } } } } /** * @private */ class TrianglesColorRenderer$1 extends TrianglesBatchingRenderer { _getHash() { const scene = this._scene; return [scene._lightsState.getHash(), scene._sectionPlanesState.getHash(), (this._withSAO ? "sao" : "nosao")].join(";"); } drawLayer(frameCtx, layer, renderPass) { super.drawLayer(frameCtx, layer, renderPass, { incrementDrawState: true }); } _buildVertexShader() { const scene = this._scene; const sectionPlanesState = scene._sectionPlanesState; const lightsState = scene._lightsState; const clipping = sectionPlanesState.getNumAllocatedSectionPlanes() > 0; let light; const src = []; src.push("#version 300 es"); src.push("// Triangles batching draw vertex shader"); src.push("uniform int renderPass;"); src.push("in vec3 position;"); src.push("in vec3 normal;"); src.push("in vec4 color;"); src.push("in float flags;"); if (scene.entityOffsetsEnabled) { src.push("in vec3 offset;"); } this._addMatricesUniformBlockLines(src, true); if (scene.logarithmicDepthBufferEnabled) { src.push("uniform float logDepthBufFC;"); src.push("out float vFragDepth;"); src.push("bool isPerspectiveMatrix(mat4 m) {"); src.push(" return (m[2][3] == - 1.0);"); src.push("}"); src.push("out float isPerspective;"); } src.push("uniform vec4 lightAmbient;"); for (let i = 0, len = lightsState.lights.length; i < len; i++) { light = lightsState.lights[i]; if (light.type === "ambient") { continue; } src.push("uniform vec4 lightColor" + i + ";"); if (light.type === "dir") { src.push("uniform vec3 lightDir" + i + ";"); } if (light.type === "point") { src.push("uniform vec3 lightPos" + i + ";"); } if (light.type === "spot") { src.push("uniform vec3 lightPos" + i + ";"); src.push("uniform vec3 lightDir" + i + ";"); } } src.push("vec3 octDecode(vec2 oct) {"); src.push(" vec3 v = vec3(oct.xy, 1.0 - abs(oct.x) - abs(oct.y));"); src.push(" if (v.z < 0.0) {"); src.push(" v.xy = (1.0 - abs(v.yx)) * vec2(v.x >= 0.0 ? 1.0 : -1.0, v.y >= 0.0 ? 1.0 : -1.0);"); src.push(" }"); src.push(" return normalize(v);"); src.push("}"); if (clipping) { src.push("out vec4 vWorldPosition;"); src.push("out float vFlags;"); } src.push("out vec4 vColor;"); src.push("void main(void) {"); // colorFlag = NOT_RENDERED | COLOR_OPAQUE | COLOR_TRANSPARENT // renderPass = COLOR_OPAQUE src.push(`int colorFlag = int(flags) & 0xF;`); src.push(`if (colorFlag != renderPass) {`); src.push(" gl_Position = vec4(0.0, 0.0, 0.0, 0.0);"); // Cull vertex src.push("} else {"); src.push("vec4 worldPosition = worldMatrix * (positionsDecodeMatrix * vec4(position, 1.0)); "); if (scene.entityOffsetsEnabled) { src.push("worldPosition.xyz = worldPosition.xyz + offset;"); } src.push("vec4 viewPosition = viewMatrix * worldPosition; "); src.push("vec4 worldNormal = worldNormalMatrix * vec4(octDecode(normal.xy), 0.0); "); src.push("vec3 viewNormal = normalize((viewNormalMatrix * worldNormal).xyz);"); src.push("vec3 reflectedColor = vec3(0.0, 0.0, 0.0);"); src.push("vec3 viewLightDir = vec3(0.0, 0.0, -1.0);"); src.push("float lambertian = 1.0;"); for (let i = 0, len = lightsState.lights.length; i < len; i++) { light = lightsState.lights[i]; if (light.type === "ambient") { continue; } if (light.type === "dir") { if (light.space === "view") { src.push("viewLightDir = normalize(lightDir" + i + ");"); } else { src.push("viewLightDir = normalize((viewMatrix * vec4(lightDir" + i + ", 0.0)).xyz);"); } } else if (light.type === "point") { if (light.space === "view") { src.push("viewLightDir = -normalize(lightPos" + i + " - viewPosition.xyz);"); } else { src.push("viewLightDir = -normalize((viewMatrix * vec4(lightPos" + i + ", 0.0)).xyz);"); } } else if (light.type === "spot") { if (light.space === "view") { src.push("viewLightDir = normalize(lightDir" + i + ");"); } else { src.push("viewLightDir = normalize((viewMatrix * vec4(lightDir" + i + ", 0.0)).xyz);"); } } else { continue; } src.push("lambertian = max(dot(-viewNormal, viewLightDir), 0.0);"); src.push("reflectedColor += lambertian * (lightColor" + i + ".rgb * lightColor" + i + ".a);"); } src.push("vec3 rgb = (vec3(float(color.r) / 255.0, float(color.g) / 255.0, float(color.b) / 255.0));"); src.push("vColor = vec4((lightAmbient.rgb * lightAmbient.a * rgb) + (reflectedColor * rgb), float(color.a) / 255.0);"); src.push("vec4 clipPos = projMatrix * viewPosition;"); if (scene.logarithmicDepthBufferEnabled) { src.push("vFragDepth = 1.0 + clipPos.w;"); src.push("isPerspective = float (isPerspectiveMatrix(projMatrix));"); } if (clipping) { src.push("vWorldPosition = worldPosition;"); src.push("vFlags = flags;"); } src.push("gl_Position = clipPos;"); src.push("}"); src.push("}"); return src; } _buildFragmentShader() { const scene = this._scene; const sectionPlanesState = scene._sectionPlanesState; const clipping = sectionPlanesState.getNumAllocatedSectionPlanes() > 0; const src = []; src.push("#version 300 es"); src.push("// Triangles batching draw fragment shader"); src.push("#ifdef GL_FRAGMENT_PRECISION_HIGH"); src.push("precision highp float;"); src.push("precision highp int;"); src.push("#else"); src.push("precision mediump float;"); src.push("precision mediump int;"); src.push("#endif"); if (scene.logarithmicDepthBufferEnabled) { src.push("in float isPerspective;"); src.push("uniform float logDepthBufFC;"); src.push("in float vFragDepth;"); } if (this._withSAO) { src.push("uniform sampler2D uOcclusionTexture;"); src.push("uniform vec4 uSAOParams;"); src.push("const float packUpscale = 256. / 255.;"); src.push("const float unpackDownScale = 255. / 256.;"); src.push("const vec3 packFactors = vec3( 256. * 256. * 256., 256. * 256., 256. );"); src.push("const vec4 unPackFactors = unpackDownScale / vec4( packFactors, 1. );"); src.push("float unpackRGBToFloat( const in vec4 v ) {"); src.push(" return dot( v, unPackFactors );"); src.push("}"); } if (clipping) { src.push("in vec4 vWorldPosition;"); src.push("in float vFlags;"); for (let i = 0, len = sectionPlanesState.getNumAllocatedSectionPlanes(); i < len; i++) { src.push("uniform bool sectionPlaneActive" + i + ";"); src.push("uniform vec3 sectionPlanePos" + i + ";"); src.push("uniform vec3 sectionPlaneDir" + i + ";"); } src.push("uniform float sliceThickness;"); src.push("uniform vec4 sliceColor;"); } src.push("in vec4 vColor;"); src.push("out vec4 outColor;"); src.push("void main(void) {"); src.push(" vec4 newColor;"); src.push(" newColor = vColor;"); if (clipping) { src.push(" bool clippable = (int(vFlags) >> 16 & 0xF) == 1;"); src.push(" if (clippable) {"); src.push(" float dist = 0.0;"); for (let i = 0, len = sectionPlanesState.getNumAllocatedSectionPlanes(); i < len; i++) { src.push("if (sectionPlaneActive" + i + ") {"); src.push(" dist += clamp(dot(-sectionPlaneDir" + i + ".xyz, vWorldPosition.xyz - sectionPlanePos" + i + ".xyz), 0.0, 1000.0);"); src.push("}"); } src.push(" if (dist > sliceThickness) { "); src.push(" discard;"); src.push(" }"); src.push(" if (dist > 0.0) { "); src.push(" newColor = sliceColor;"); src.push(" }"); src.push("}"); } if (scene.logarithmicDepthBufferEnabled) { src.push(" float dx = dFdx(vFragDepth);"); src.push(" float dy = dFdy(vFragDepth);"); src.push(" float diff = sqrt(dx*dx+dy*dy);"); src.push(" gl_FragDepth = isPerspective == 0.0 ? gl_FragCoord.z : log2( vFragDepth + diff ) * logDepthBufFC * 0.5;"); } else { src.push(" float dx = dFdx(gl_FragCoord.z);"); src.push(" float dy = dFdy(gl_FragCoord.z);"); src.push(" float diff = sqrt(dx*dx+dy*dy);"); src.push(" gl_FragDepth = gl_FragCoord.z + diff;"); } if (this._withSAO) { // Doing SAO blend in the main solid fill draw shader just so that edge lines can be drawn over the top // Would be more efficient to defer this, then render lines later, using same depth buffer for Z-reject src.push(" float viewportWidth = uSAOParams[0];"); src.push(" float viewportHeight = uSAOParams[1];"); src.push(" float blendCutoff = uSAOParams[2];"); src.push(" float blendFactor = uSAOParams[3];"); src.push(" vec2 uv = vec2(gl_FragCoord.x / viewportWidth, gl_FragCoord.y / viewportHeight);"); src.push(" float ambient = smoothstep(blendCutoff, 1.0, unpackRGBToFloat(texture(uOcclusionTexture, uv))) * blendFactor;"); src.push(" outColor = vec4(newColor.rgb * ambient, 1.0);"); } else { src.push(" outColor = newColor;"); } src.push("}"); return src; } } /** * @private */ class TrianglesFlatColorRenderer$1 extends TrianglesBatchingRenderer { _getHash() { const scene = this._scene; return [scene._lightsState.getHash(), scene._sectionPlanesState.getHash(), (this._withSAO ? "sao" : "nosao")].join(";"); } _buildVertexShader() { const scene = this._scene; const sectionPlanesState = scene._sectionPlanesState; const clipping = sectionPlanesState.getNumAllocatedSectionPlanes() > 0; const src = []; src.push("#version 300 es"); src.push("// Triangles batching flat-shading draw vertex shader"); src.push("uniform int renderPass;"); src.push("in vec3 position;"); src.push("in vec4 color;"); src.push("in float flags;"); if (scene.entityOffsetsEnabled) { src.push("in vec3 offset;"); } this._addMatricesUniformBlockLines(src); if (scene.logarithmicDepthBufferEnabled) { src.push("uniform float logDepthBufFC;"); src.push("out float vFragDepth;"); src.push("bool isPerspectiveMatrix(mat4 m) {"); src.push(" return (m[2][3] == - 1.0);"); src.push("}"); src.push("out float isPerspective;"); } if (clipping) { src.push("out vec4 vWorldPosition;"); src.push("out float vFlags;"); } src.push("out vec4 vViewPosition;"); src.push("out vec4 vColor;"); src.push("void main(void) {"); // colorFlag = NOT_RENDERED | COLOR_OPAQUE | COLOR_TRANSPARENT // renderPass = COLOR_OPAQUE src.push(`int colorFlag = int(flags) & 0xF;`); src.push(`if (colorFlag != renderPass) {`); src.push(" gl_Position = vec4(0.0, 0.0, 0.0, 0.0);"); // Cull vertex src.push("} else {"); src.push("vec4 worldPosition = worldMatrix * (positionsDecodeMatrix * vec4(position, 1.0)); "); if (scene.entityOffsetsEnabled) { src.push("worldPosition.xyz = worldPosition.xyz + offset;"); } src.push("vec4 viewPosition = viewMatrix * worldPosition; "); src.push("vViewPosition = viewPosition;"); src.push("vColor = vec4(float(color.r) / 255.0, float(color.g) / 255.0, float(color.b) / 255.0, float(color.a) / 255.0);"); src.push("vec4 clipPos = projMatrix * viewPosition;"); if (scene.logarithmicDepthBufferEnabled) { src.push("vFragDepth = 1.0 + clipPos.w;"); src.push("isPerspective = float (isPerspectiveMatrix(projMatrix));"); } if (clipping) { src.push("vWorldPosition = worldPosition;"); src.push("vFlags = flags;"); } src.push("gl_Position = clipPos;"); src.push("}"); src.push("}"); return src; } _buildFragmentShader() { const scene = this._scene; const lightsState = scene._lightsState; const sectionPlanesState = scene._sectionPlanesState; const clipping = sectionPlanesState.getNumAllocatedSectionPlanes() > 0; const src = []; src.push("#version 300 es"); src.push("// Triangles batching flat-shading draw fragment shader"); src.push("#ifdef GL_FRAGMENT_PRECISION_HIGH"); src.push("precision highp float;"); src.push("precision highp int;"); src.push("#else"); src.push("precision mediump float;"); src.push("precision mediump int;"); src.push("#endif"); if (scene.logarithmicDepthBufferEnabled) { src.push("in float isPerspective;"); src.push("uniform float logDepthBufFC;"); src.push("in float vFragDepth;"); } if (this._withSAO) { src.push("uniform sampler2D uOcclusionTexture;"); src.push("uniform vec4 uSAOParams;"); src.push("const float packUpscale = 256. / 255.;"); src.push("const float unpackDownScale = 255. / 256.;"); src.push("const vec3 packFactors = vec3( 256. * 256. * 256., 256. * 256., 256. );"); src.push("const vec4 unPackFactors = unpackDownScale / vec4( packFactors, 1. );"); src.push("float unpackRGBToFloat( const in vec4 v ) {"); src.push(" return dot( v, unPackFactors );"); src.push("}"); } if (clipping) { src.push("in vec4 vWorldPosition;"); src.push("in float vFlags;"); for (let i = 0, len = sectionPlanesState.getNumAllocatedSectionPlanes(); i < len; i++) { src.push("uniform bool sectionPlaneActive" + i + ";"); src.push("uniform vec3 sectionPlanePos" + i + ";"); src.push("uniform vec3 sectionPlaneDir" + i + ";"); } src.push("uniform float sliceThickness;"); src.push("uniform vec4 sliceColor;"); } this._addMatricesUniformBlockLines(src); src.push("uniform vec4 lightAmbient;"); for (let i = 0, len = lightsState.lights.length; i < len; i++) { const light = lightsState.lights[i]; if (light.type === "ambient") { continue; } src.push("uniform vec4 lightColor" + i + ";"); if (light.type === "dir") { src.push("uniform vec3 lightDir" + i + ";"); } if (light.type === "point") { src.push("uniform vec3 lightPos" + i + ";"); } if (light.type === "spot") { src.push("uniform vec3 lightPos" + i + ";"); src.push("uniform vec3 lightDir" + i + ";"); } } src.push("in vec4 vViewPosition;"); src.push("in vec4 vColor;"); src.push("out vec4 outColor;"); src.push("void main(void) {"); src.push(" vec4 newColor;"); src.push(" newColor = vColor;"); if (clipping) { src.push(" bool clippable = (int(vFlags) >> 16 & 0xF) == 1;"); src.push(" if (clippable) {"); src.push(" float dist = 0.0;"); for (let i = 0, len = sectionPlanesState.getNumAllocatedSectionPlanes(); i < len; i++) { src.push("if (sectionPlaneActive" + i + ") {"); src.push(" dist += clamp(dot(-sectionPlaneDir" + i + ".xyz, vWorldPosition.xyz - sectionPlanePos" + i + ".xyz), 0.0, 1000.0);"); src.push("}"); } src.push(" if (dist > sliceThickness) { "); src.push(" discard;"); src.push(" }"); src.push(" if (dist > 0.0) { "); src.push(" newColor = sliceColor;"); src.push(" }"); src.push("}"); } src.push("vec3 reflectedColor = vec3(0.0, 0.0, 0.0);"); src.push("vec3 viewLightDir = vec3(0.0, 0.0, -1.0);"); src.push("float lambertian = 1.0;"); src.push("vec3 xTangent = dFdx( vViewPosition.xyz );"); src.push("vec3 yTangent = dFdy( vViewPosition.xyz );"); src.push("vec3 viewNormal = normalize( cross( xTangent, yTangent ) );"); for (let i = 0, len = lightsState.lights.length; i < len; i++) { const light = lightsState.lights[i]; if (light.type === "ambient") { continue; } if (light.type === "dir") { if (light.space === "view") { src.push("viewLightDir = normalize(lightDir" + i + ");"); } else { src.push("viewLightDir = normalize((viewMatrix * vec4(lightDir" + i + ", 0.0)).xyz);"); } } else if (light.type === "point") { if (light.space === "view") { src.push("viewLightDir = -normalize(lightPos" + i + " - viewPosition.xyz);"); } else { src.push("viewLightDir = -normalize((viewMatrix * vec4(lightPos" + i + ", 0.0)).xyz);"); } } else if (light.type === "spot") { if (light.space === "view") { src.push("viewLightDir = normalize(lightDir" + i + ");"); } else { src.push("viewLightDir = normalize((viewMatrix * vec4(lightDir" + i + ", 0.0)).xyz);"); } } else { continue; } src.push("lambertian = max(dot(-viewNormal, viewLightDir), 0.0);"); src.push("reflectedColor += lambertian * (lightColor" + i + ".rgb * lightColor" + i + ".a);"); } src.push("vec4 fragColor = vec4((lightAmbient.rgb * lightAmbient.a * newColor.rgb) + (reflectedColor * newColor.rgb), newColor.a);"); if (this._withSAO) { // Doing SAO blend in the main solid fill draw shader just so that edge lines can be drawn over the top // Would be more efficient to defer this, then render lines later, using same depth buffer for Z-reject src.push(" float viewportWidth = uSAOParams[0];"); src.push(" float viewportHeight = uSAOParams[1];"); src.push(" float blendCutoff = uSAOParams[2];"); src.push(" float blendFactor = uSAOParams[3];"); src.push(" vec2 uv = vec2(gl_FragCoord.x / viewportWidth, gl_FragCoord.y / viewportHeight);"); src.push(" float ambient = smoothstep(blendCutoff, 1.0, unpackRGBToFloat(texture(uOcclusionTexture, uv))) * blendFactor;"); src.push(" outColor = vec4(fragColor.rgb * ambient, 1.0);"); } else { src.push(" outColor = fragColor;"); } if (scene.logarithmicDepthBufferEnabled) { src.push(" gl_FragDepth = isPerspective == 0.0 ? gl_FragCoord.z : log2( vFragDepth ) * logDepthBufFC * 0.5;"); } else { src.push(" float dx = dFdx(gl_FragCoord.z);"); src.push(" float dy = dFdy(gl_FragCoord.z);"); src.push(" float diff = sqrt(dx*dx+dy*dy);"); src.push(" gl_FragDepth = gl_FragCoord.z + diff;"); } src.push("}"); return src; } } /** * @private */ class TrianglesSilhouetteRenderer$1 extends TrianglesBatchingRenderer { drawLayer(frameCtx, batchingLayer, renderPass) { super.drawLayer(frameCtx, batchingLayer, renderPass, { colorUniform: true }); } _buildVertexShader() { const scene = this._scene; const sectionPlanesState = scene._sectionPlanesState; const clipping = sectionPlanesState.getNumAllocatedSectionPlanes() > 0; const src = []; src.push("#version 300 es"); src.push("// Triangles batching silhouette vertex shader"); src.push("uniform int renderPass;"); src.push("in vec3 position;"); if (scene.entityOffsetsEnabled) { src.push("in vec3 offset;"); } src.push("in float flags;"); src.push("in vec4 color;"); this._addMatricesUniformBlockLines(src); src.push("uniform vec4 silhouetteColor;"); if (scene.logarithmicDepthBufferEnabled) { src.push("uniform float logDepthBufFC;"); src.push("out float vFragDepth;"); src.push("bool isPerspectiveMatrix(mat4 m) {"); src.push(" return (m[2][3] == - 1.0);"); src.push("}"); src.push("out float isPerspective;"); } if (clipping) { src.push("out vec4 vWorldPosition;"); src.push("out float vFlags;"); } src.push("out vec4 vColor;"); src.push("void main(void) {"); // silhouetteFlag = NOT_RENDERED | SILHOUETTE_HIGHLIGHTED | SILHOUETTE_SELECTED | SILHOUETTE_XRAYED // renderPass = SILHOUETTE_HIGHLIGHTED | SILHOUETTE_SELECTED | | SILHOUETTE_XRAYED src.push(`int silhouetteFlag = int(flags) >> 4 & 0xF;`); src.push(`if (silhouetteFlag != renderPass) {`); src.push(" gl_Position = vec4(0.0, 0.0, 0.0, 0.0);"); // Cull vertex src.push("} else {"); src.push(" vec4 worldPosition = worldMatrix * (positionsDecodeMatrix * vec4(position, 1.0)); "); if (scene.entityOffsetsEnabled) { src.push(" worldPosition.xyz = worldPosition.xyz + offset;"); } src.push("vec4 viewPosition = viewMatrix * worldPosition; "); if (clipping) { src.push("vWorldPosition = worldPosition;"); src.push("vFlags = flags;"); } src.push("vColor = vec4(silhouetteColor.r, silhouetteColor.g, silhouetteColor.b, min(silhouetteColor.a, color.a ));"); src.push("vec4 clipPos = projMatrix * viewPosition;"); if (scene.logarithmicDepthBufferEnabled) { src.push("vFragDepth = 1.0 + clipPos.w;"); src.push("isPerspective = float (isPerspectiveMatrix(projMatrix));"); } src.push("gl_Position = clipPos;"); src.push("}"); src.push("}"); return src; } _buildFragmentShader() { const scene = this._scene; const sectionPlanesState = scene._sectionPlanesState; let i; let len; const clipping = sectionPlanesState.getNumAllocatedSectionPlanes() > 0; const src = []; src.push("#version 300 es"); src.push("// Triangles batching silhouette fragment shader"); src.push("#ifdef GL_FRAGMENT_PRECISION_HIGH"); src.push("precision highp float;"); src.push("precision highp int;"); src.push("#else"); src.push("precision mediump float;"); src.push("precision mediump int;"); src.push("#endif"); if (scene.logarithmicDepthBufferEnabled) { src.push("in float isPerspective;"); src.push("uniform float logDepthBufFC;"); src.push("in float vFragDepth;"); } if (clipping) { src.push("in vec4 vWorldPosition;"); src.push("in float vFlags;"); for (i = 0, len = sectionPlanesState.getNumAllocatedSectionPlanes(); i < len; i++) { src.push("uniform bool sectionPlaneActive" + i + ";"); src.push("uniform vec3 sectionPlanePos" + i + ";"); src.push("uniform vec3 sectionPlaneDir" + i + ";"); } src.push("uniform float sliceThickness;"); src.push("uniform vec4 sliceColor;"); } src.push("in vec4 vColor;"); src.push("out vec4 outColor;"); src.push("void main(void) {"); src.push(" vec4 newColor;"); src.push(" newColor = vColor;"); if (clipping) { src.push(" bool clippable = (int(vFlags) >> 16 & 0xF) == 1;"); src.push(" if (clippable) {"); src.push(" float dist = 0.0;"); for (let i = 0, len = sectionPlanesState.getNumAllocatedSectionPlanes(); i < len; i++) { src.push("if (sectionPlaneActive" + i + ") {"); src.push(" dist += clamp(dot(-sectionPlaneDir" + i + ".xyz, vWorldPosition.xyz - sectionPlanePos" + i + ".xyz), 0.0, 1000.0);"); src.push("}"); } src.push(" if (dist > sliceThickness) { "); src.push(" discard;"); src.push(" }"); src.push(" if (dist > 0.0) { "); src.push(" newColor = sliceColor;"); src.push(" }"); src.push("}"); } if (scene.logarithmicDepthBufferEnabled) { src.push(" gl_FragDepth = isPerspective == 0.0 ? gl_FragCoord.z : log2( vFragDepth ) * logDepthBufFC * 0.5;"); } src.push("outColor = newColor;"); src.push("}"); return src; } } /** * @private */ class EdgesRenderer$1 extends TrianglesBatchingRenderer { constructor(scene) { super(scene, false, {instancing: false, edges: true}); } } /** * @private */ class EdgesEmphasisRenderer$1 extends EdgesRenderer$1 { drawLayer(frameCtx, batchingLayer, renderPass) { super.drawLayer(frameCtx, batchingLayer, renderPass, { colorUniform: true }); } _buildVertexShader() { const scene = this._scene; const sectionPlanesState = scene._sectionPlanesState; const clipping = sectionPlanesState.getNumAllocatedSectionPlanes() > 0; const src = []; src.push("#version 300 es"); src.push("// EdgesEmphasisRenderer vertex shader"); src.push("uniform int renderPass;"); src.push("uniform vec4 color;"); src.push("in vec3 position;"); if (scene.entityOffsetsEnabled) { src.push("in vec3 offset;"); } src.push("in float flags;"); this._addMatricesUniformBlockLines(src); if (scene.logarithmicDepthBufferEnabled) { src.push("uniform float logDepthBufFC;"); src.push("out float vFragDepth;"); src.push("bool isPerspectiveMatrix(mat4 m) {"); src.push(" return (m[2][3] == - 1.0);"); src.push("}"); src.push("out float isPerspective;"); } if (clipping) { src.push("out vec4 vWorldPosition;"); src.push("out float vFlags;"); } src.push("out vec4 vColor;"); src.push("void main(void) {"); // edgeFlag = NOT_RENDERED | EDGES_COLOR_OPAQUE | EDGES_COLOR_TRANSPARENT | EDGES_HIGHLIGHTED | EDGES_XRAYED | EDGES_SELECTED // renderPass = EDGES_COLOR_OPAQUE | EDGES_COLOR_TRANSPARENT | EDGES_HIGHLIGHTED | EDGES_XRAYED | EDGES_SELECTED src.push(`int edgeFlag = int(flags) >> 8 & 0xF;`); src.push(`if (edgeFlag != renderPass) {`); src.push(" gl_Position = vec4(0.0, 0.0, 0.0, 0.0);"); // Cull vertex src.push("} else {"); src.push(" vec4 worldPosition = worldMatrix * (positionsDecodeMatrix * vec4(position, 1.0)); "); if (scene.entityOffsetsEnabled) { src.push(" worldPosition.xyz = worldPosition.xyz + offset;"); } src.push(" vec4 viewPosition = viewMatrix * worldPosition; "); if (clipping) { src.push(" vWorldPosition = worldPosition;"); src.push(" vFlags = flags;"); } src.push("vec4 clipPos = projMatrix * viewPosition;"); if (scene.logarithmicDepthBufferEnabled) { src.push("vFragDepth = 1.0 + clipPos.w;"); src.push("isPerspective = float (isPerspectiveMatrix(projMatrix));"); } src.push("gl_Position = clipPos;"); src.push("vColor = vec4(color.r, color.g, color.b, color.a);"); src.push("}"); src.push("}"); return src; } _buildFragmentShader() { const scene = this._scene; const sectionPlanesState = scene._sectionPlanesState; const clipping = sectionPlanesState.getNumAllocatedSectionPlanes() > 0; const src = []; src.push("#version 300 es"); src.push("// EdgesEmphasisRenderer fragment shader"); src.push("#ifdef GL_FRAGMENT_PRECISION_HIGH"); src.push("precision highp float;"); src.push("precision highp int;"); src.push("#else"); src.push("precision mediump float;"); src.push("precision mediump int;"); src.push("#endif"); if (scene.logarithmicDepthBufferEnabled) { src.push("in float isPerspective;"); src.push("uniform float logDepthBufFC;"); src.push("in float vFragDepth;"); } if (clipping) { src.push("in vec4 vWorldPosition;"); src.push("in float vFlags;"); for (let i = 0, len = sectionPlanesState.getNumAllocatedSectionPlanes(); i < len; i++) { src.push("uniform bool sectionPlaneActive" + i + ";"); src.push("uniform vec3 sectionPlanePos" + i + ";"); src.push("uniform vec3 sectionPlaneDir" + i + ";"); } } src.push("in vec4 vColor;"); src.push("out vec4 outColor;"); src.push("void main(void) {"); if (clipping) { src.push(" bool clippable = (int(vFlags) >> 16 & 0xF) == 1;"); src.push(" if (clippable) {"); src.push(" float dist = 0.0;"); for (let i = 0, len = sectionPlanesState.getNumAllocatedSectionPlanes(); i < len; i++) { src.push("if (sectionPlaneActive" + i + ") {"); src.push(" dist += clamp(dot(-sectionPlaneDir" + i + ".xyz, vWorldPosition.xyz - sectionPlanePos" + i + ".xyz), 0.0, 1000.0);"); src.push("}"); } src.push(" if (dist > 0.0) { discard; }"); src.push("}"); } if (scene.logarithmicDepthBufferEnabled) { src.push(" gl_FragDepth = isPerspective == 0.0 ? gl_FragCoord.z : log2( vFragDepth ) * logDepthBufFC * 0.5;"); } src.push("outColor = vColor;"); src.push("}"); return src; } } /** * @private */ class EdgesColorRenderer$1 extends EdgesRenderer$1 { drawLayer(frameCtx, batchingLayer, renderPass) { super.drawLayer(frameCtx, batchingLayer, renderPass, { colorUniform: false }); } _buildVertexShader() { const scene = this._scene; const sectionPlanesState = scene._sectionPlanesState; const clipping = sectionPlanesState.getNumAllocatedSectionPlanes() > 0; const src = []; src.push("#version 300 es"); src.push("// Batched geometry edges drawing vertex shader"); src.push("uniform int renderPass;"); src.push("in vec3 position;"); src.push("in vec4 color;"); if (scene.entityOffsetsEnabled) { src.push("in vec3 offset;"); } src.push("in float flags;"); this._addMatricesUniformBlockLines(src); if (scene.logarithmicDepthBufferEnabled) { src.push("uniform float logDepthBufFC;"); src.push("out float vFragDepth;"); src.push("bool isPerspectiveMatrix(mat4 m) {"); src.push(" return (m[2][3] == - 1.0);"); src.push("}"); src.push("out float isPerspective;"); } if (clipping) { src.push("out vec4 vWorldPosition;"); src.push("out float vFlags;"); } src.push("out vec4 vColor;"); src.push("void main(void) {"); // edgeFlag = NOT_RENDERED | EDGES_COLOR_OPAQUE | EDGES_COLOR_TRANSPARENT | EDGES_HIGHLIGHTED | EDGES_XRAYED | EDGES_SELECTED // renderPass = EDGES_COLOR_OPAQUE | EDGES_COLOR_TRANSPARENT src.push(`int edgeFlag = int(flags) >> 8 & 0xF;`); src.push(`if (edgeFlag != renderPass) {`); src.push(" gl_Position = vec4(0.0, 0.0, 0.0, 0.0);"); // Cull vertex src.push("} else {"); src.push(" vec4 worldPosition = worldMatrix * (positionsDecodeMatrix * vec4(position, 1.0)); "); if (scene.entityOffsetsEnabled) { src.push(" worldPosition.xyz = worldPosition.xyz + offset;"); } src.push(" vec4 viewPosition = viewMatrix * worldPosition; "); if (clipping) { src.push(" vWorldPosition = worldPosition;"); src.push(" vFlags = flags;"); } src.push("vec4 clipPos = projMatrix * viewPosition;"); if (scene.logarithmicDepthBufferEnabled) { src.push("vFragDepth = 1.0 + clipPos.w;"); src.push("isPerspective = float (isPerspectiveMatrix(projMatrix));"); } src.push("gl_Position = clipPos;"); //src.push("vColor = vec4(float(color.r-100.0) / 255.0, float(color.g-100.0) / 255.0, float(color.b-100.0) / 255.0, float(color.a) / 255.0);"); src.push("vColor = vec4(float(color.r*0.5) / 255.0, float(color.g*0.5) / 255.0, float(color.b*0.5) / 255.0, float(color.a) / 255.0);"); src.push("}"); src.push("}"); return src; } _buildFragmentShader() { const scene = this._scene; const sectionPlanesState = scene._sectionPlanesState; const clipping = sectionPlanesState.getNumAllocatedSectionPlanes() > 0; const src = []; src.push("#version 300 es"); src.push("// Batched geometry edges drawing fragment shader"); src.push("#ifdef GL_FRAGMENT_PRECISION_HIGH"); src.push("precision highp float;"); src.push("precision highp int;"); src.push("#else"); src.push("precision mediump float;"); src.push("precision mediump int;"); src.push("#endif"); if (scene.logarithmicDepthBufferEnabled) { src.push("in float isPerspective;"); src.push("uniform float logDepthBufFC;"); src.push("in float vFragDepth;"); } if (clipping) { src.push("in vec4 vWorldPosition;"); src.push("in float vFlags;"); for (let i = 0, len = sectionPlanesState.getNumAllocatedSectionPlanes(); i < len; i++) { src.push("uniform bool sectionPlaneActive" + i + ";"); src.push("uniform vec3 sectionPlanePos" + i + ";"); src.push("uniform vec3 sectionPlaneDir" + i + ";"); } } src.push("in vec4 vColor;"); src.push("out vec4 outColor;"); src.push("void main(void) {"); if (clipping) { src.push(" bool clippable = (int(vFlags) >> 16 & 0xF) == 1;"); src.push(" if (clippable) {"); src.push(" float dist = 0.0;"); for (let i = 0, len = sectionPlanesState.getNumAllocatedSectionPlanes(); i < len; i++) { src.push("if (sectionPlaneActive" + i + ") {"); src.push(" dist += clamp(dot(-sectionPlaneDir" + i + ".xyz, vWorldPosition.xyz - sectionPlanePos" + i + ".xyz), 0.0, 1000.0);"); src.push("}"); } src.push(" if (dist > 0.0) { discard; }"); src.push("}"); } if (scene.logarithmicDepthBufferEnabled) { src.push(" gl_FragDepth = isPerspective == 0.0 ? gl_FragCoord.z : log2( vFragDepth ) * logDepthBufFC * 0.5;"); } src.push("outColor = vColor;"); src.push("}"); return src; } } /** * @private */ class TrianglesPickMeshRenderer$1 extends TrianglesBatchingRenderer { _buildVertexShader() { const scene = this._scene; const clipping = scene._sectionPlanesState.getNumAllocatedSectionPlanes() > 0; const src = []; src.push("#version 300 es"); src.push("// Batched geometry picking vertex shader"); src.push("uniform int renderPass;"); src.push("in vec3 position;"); if (scene.entityOffsetsEnabled) { src.push("in vec3 offset;"); } src.push("in float flags;"); src.push("in vec4 pickColor;"); src.push("uniform bool pickInvisible;"); this._addMatricesUniformBlockLines(src); if (scene.logarithmicDepthBufferEnabled) { src.push("uniform float logDepthBufFC;"); src.push("out float vFragDepth;"); src.push("bool isPerspectiveMatrix(mat4 m) {"); src.push(" return (m[2][3] == - 1.0);"); src.push("}"); src.push("out float isPerspective;"); } if (clipping) { src.push("out vec4 vWorldPosition;"); src.push("out float vFlags;"); } this._addRemapClipPosLines(src); src.push("out vec4 vPickColor;"); src.push("void main(void) {"); // pickFlag = NOT_RENDERED | PICK // renderPass = PICK src.push(`int pickFlag = int(flags) >> 12 & 0xF;`); src.push(`if (pickFlag != renderPass) {`); src.push(" gl_Position = vec4(0.0, 0.0, 0.0, 0.0);"); // Cull vertex src.push(" } else {"); src.push(" vec4 worldPosition = worldMatrix * (positionsDecodeMatrix * vec4(position, 1.0)); "); if (scene.entityOffsetsEnabled) { src.push(" worldPosition.xyz = worldPosition.xyz + offset;"); } src.push(" vec4 viewPosition = viewMatrix * worldPosition; "); src.push(" vPickColor = vec4(float(pickColor.r) / 255.0, float(pickColor.g) / 255.0, float(pickColor.b) / 255.0, float(pickColor.a) / 255.0);"); if (clipping) { src.push(" vWorldPosition = worldPosition;"); src.push(" vFlags = flags;"); } src.push("vec4 clipPos = projMatrix * viewPosition;"); if (scene.logarithmicDepthBufferEnabled) { src.push("vFragDepth = 1.0 + clipPos.w;"); src.push("isPerspective = float (isPerspectiveMatrix(projMatrix));"); } src.push("gl_Position = remapClipPos(clipPos);"); src.push(" }"); src.push("}"); return src; } _buildFragmentShader() { const scene = this._scene; const sectionPlanesState = scene._sectionPlanesState; const clipping = sectionPlanesState.getNumAllocatedSectionPlanes() > 0; const src = []; src.push("#version 300 es"); src.push("// Batched geometry picking fragment shader"); src.push("#ifdef GL_FRAGMENT_PRECISION_HIGH"); src.push("precision highp float;"); src.push("precision highp int;"); src.push("#else"); src.push("precision mediump float;"); src.push("precision mediump int;"); src.push("#endif"); if (scene.logarithmicDepthBufferEnabled) { src.push("in float isPerspective;"); src.push("uniform float logDepthBufFC;"); src.push("in float vFragDepth;"); } if (clipping) { src.push("in vec4 vWorldPosition;"); src.push("in float vFlags;"); for (var i = 0; i < sectionPlanesState.getNumAllocatedSectionPlanes(); i++) { src.push("uniform bool sectionPlaneActive" + i + ";"); src.push("uniform vec3 sectionPlanePos" + i + ";"); src.push("uniform vec3 sectionPlaneDir" + i + ";"); } } src.push("in vec4 vPickColor;"); src.push("out vec4 outColor;"); src.push("void main(void) {"); if (clipping) { src.push(" bool clippable = (int(vFlags) >> 16 & 0xF) == 1;"); src.push(" if (clippable) {"); src.push(" float dist = 0.0;"); for (let i = 0; i < sectionPlanesState.getNumAllocatedSectionPlanes(); i++) { src.push(" if (sectionPlaneActive" + i + ") {"); src.push(" dist += clamp(dot(-sectionPlaneDir" + i + ".xyz, vWorldPosition.xyz - sectionPlanePos" + i + ".xyz), 0.0, 1000.0);"); src.push(" }"); } src.push(" if (dist > 0.0) { discard; }"); src.push(" }"); } if (scene.logarithmicDepthBufferEnabled) { src.push(" gl_FragDepth = isPerspective == 0.0 ? gl_FragCoord.z : log2( vFragDepth ) * logDepthBufFC * 0.5;"); } src.push(" outColor = vPickColor; "); src.push("}"); return src; } } /** * @private */ class TrianglesPickDepthRenderer$1 extends TrianglesBatchingRenderer { _buildVertexShader() { const scene = this._scene; const clipping = scene._sectionPlanesState.getNumAllocatedSectionPlanes() > 0; const src = []; src.push("#version 300 es"); src.push("// Triangles batching pick depth vertex shader"); src.push("uniform int renderPass;"); src.push("in vec3 position;"); if (scene.entityOffsetsEnabled) { src.push("in vec3 offset;"); } src.push("in float flags;"); src.push("uniform bool pickInvisible;"); this._addMatricesUniformBlockLines(src); if (scene.logarithmicDepthBufferEnabled) { src.push("uniform float logDepthBufFC;"); src.push("out float vFragDepth;"); src.push("bool isPerspectiveMatrix(mat4 m) {"); src.push(" return (m[2][3] == - 1.0);"); src.push("}"); src.push("out float isPerspective;"); } if (clipping) { src.push("out vec4 vWorldPosition;"); src.push("out float vFlags;"); } this._addRemapClipPosLines(src); src.push("out vec4 vViewPosition;"); src.push("void main(void) {"); // pickFlag = NOT_RENDERED | PICK // renderPass = PICK src.push(`int pickFlag = int(flags) >> 12 & 0xF;`); src.push(`if (pickFlag != renderPass) {`); src.push(" gl_Position = vec4(0.0, 0.0, 0.0, 0.0);"); // Cull vertex src.push(" } else {"); src.push(" vec4 worldPosition = worldMatrix * (positionsDecodeMatrix * vec4(position, 1.0)); "); if (scene.entityOffsetsEnabled) { src.push(" worldPosition.xyz = worldPosition.xyz + offset;"); } src.push(" vec4 viewPosition = viewMatrix * worldPosition; "); if (clipping) { src.push(" vWorldPosition = worldPosition;"); src.push(" vFlags = flags;"); } src.push("vViewPosition = viewPosition;"); src.push("vec4 clipPos = projMatrix * viewPosition;"); if (scene.logarithmicDepthBufferEnabled) { src.push("vFragDepth = 1.0 + clipPos.w;"); src.push("isPerspective = float (isPerspectiveMatrix(projMatrix));"); } src.push("gl_Position = remapClipPos(clipPos);"); src.push(" }"); src.push("}"); return src; } _buildFragmentShader() { const scene = this._scene; const sectionPlanesState = scene._sectionPlanesState; const clipping = sectionPlanesState.getNumAllocatedSectionPlanes() > 0; const src = []; src.push("#version 300 es"); src.push("// Triangles batching pick depth fragment shader"); src.push("#ifdef GL_FRAGMENT_PRECISION_HIGH"); src.push("precision highp float;"); src.push("precision highp int;"); src.push("#else"); src.push("precision mediump float;"); src.push("precision mediump int;"); src.push("#endif"); if (scene.logarithmicDepthBufferEnabled) { src.push("in float isPerspective;"); src.push("uniform float logDepthBufFC;"); src.push("in float vFragDepth;"); } src.push("uniform float pickZNear;"); src.push("uniform float pickZFar;"); if (clipping) { src.push("in vec4 vWorldPosition;"); src.push("in float vFlags;"); for (var i = 0; i < sectionPlanesState.getNumAllocatedSectionPlanes(); i++) { src.push("uniform bool sectionPlaneActive" + i + ";"); src.push("uniform vec3 sectionPlanePos" + i + ";"); src.push("uniform vec3 sectionPlaneDir" + i + ";"); } } src.push("in vec4 vViewPosition;"); src.push("vec4 packDepth(const in float depth) {"); src.push(" const vec4 bitShift = vec4(256.0*256.0*256.0, 256.0*256.0, 256.0, 1.0);"); src.push(" const vec4 bitMask = vec4(0.0, 1.0/256.0, 1.0/256.0, 1.0/256.0);"); src.push(" vec4 res = fract(depth * bitShift);"); src.push(" res -= res.xxyz * bitMask;"); src.push(" return res;"); src.push("}"); src.push("out vec4 outColor;"); src.push("void main(void) {"); if (clipping) { src.push(" bool clippable = (int(vFlags) >> 16 & 0xF) == 1;"); src.push(" if (clippable) {"); src.push(" float dist = 0.0;"); for (let i = 0; i < sectionPlanesState.getNumAllocatedSectionPlanes(); i++) { src.push(" if (sectionPlaneActive" + i + ") {"); src.push(" dist += clamp(dot(-sectionPlaneDir" + i + ".xyz, vWorldPosition.xyz - sectionPlanePos" + i + ".xyz), 0.0, 1000.0);"); src.push(" }"); } src.push(" if (dist > 0.0) { discard; }"); src.push(" }"); } if (scene.logarithmicDepthBufferEnabled) { src.push(" gl_FragDepth = isPerspective == 0.0 ? gl_FragCoord.z : log2( vFragDepth ) * logDepthBufFC * 0.5;"); } src.push(" float zNormalizedDepth = abs((pickZNear + vViewPosition.z) / (pickZFar - pickZNear));"); src.push(" outColor = packDepth(zNormalizedDepth); "); // Must be linear depth src.push("}"); return src; } } /** * @private */ class TrianglesPickNormalsRenderer$1 extends TrianglesBatchingRenderer { _buildVertexShader() { const scene = this._scene; const clipping = scene._sectionPlanesState.getNumAllocatedSectionPlanes() > 0; const src = []; src.push("#version 300 es"); src.push("// Triangles batching pick normals vertex shader"); src.push("uniform int renderPass;"); src.push("in vec3 position;"); if (scene.entityOffsetsEnabled) { src.push("in vec3 offset;"); } src.push("in vec3 normal;"); src.push("in float flags;"); src.push("uniform bool pickInvisible;"); this._addMatricesUniformBlockLines(src); this._addRemapClipPosLines(src, 3); if (scene.logarithmicDepthBufferEnabled) { src.push("uniform float logDepthBufFC;"); src.push("out float vFragDepth;"); src.push("bool isPerspectiveMatrix(mat4 m) {"); src.push(" return (m[2][3] == - 1.0);"); src.push("}"); src.push("out float isPerspective;"); } src.push("vec3 octDecode(vec2 oct) {"); src.push(" vec3 v = vec3(oct.xy, 1.0 - abs(oct.x) - abs(oct.y));"); src.push(" if (v.z < 0.0) {"); src.push(" v.xy = (1.0 - abs(v.yx)) * vec2(v.x >= 0.0 ? 1.0 : -1.0, v.y >= 0.0 ? 1.0 : -1.0);"); src.push(" }"); src.push(" return normalize(v);"); src.push("}"); if (clipping) { src.push("out vec4 vWorldPosition;"); src.push("out float vFlags;"); } src.push("out vec3 vWorldNormal;"); src.push("out vec4 outColor;"); src.push("void main(void) {"); // pickFlag = NOT_RENDERED | PICK // renderPass = PICK src.push(`int pickFlag = int(flags) >> 12 & 0xF;`); src.push(`if (pickFlag != renderPass) {`); src.push(" gl_Position = vec4(0.0, 0.0, 0.0, 0.0);"); // Cull vertex src.push(" } else {"); src.push(" vec4 worldPosition = worldMatrix * (positionsDecodeMatrix * vec4(position, 1.0)); "); if (scene.entityOffsetsEnabled) { src.push(" worldPosition.xyz = worldPosition.xyz + offset;"); } src.push(" vec4 viewPosition = viewMatrix * worldPosition; "); src.push(" vec3 worldNormal = octDecode(normal.xy); "); src.push(" vWorldNormal = worldNormal;"); if (clipping) { src.push(" vWorldPosition = worldPosition;"); src.push(" vFlags = flags;"); } src.push("vec4 clipPos = projMatrix * viewPosition;"); if (scene.logarithmicDepthBufferEnabled) { src.push("vFragDepth = 1.0 + clipPos.w;"); src.push("isPerspective = float (isPerspectiveMatrix(projMatrix));"); } src.push("gl_Position = remapClipPos(clipPos);"); src.push(" }"); src.push("}"); return src; } _buildFragmentShader() { const scene = this._scene; const sectionPlanesState = scene._sectionPlanesState; const clipping = sectionPlanesState.getNumAllocatedSectionPlanes() > 0; const src = []; src.push("#version 300 es"); src.push("// Triangles batching pick normals fragment shader"); src.push("#ifdef GL_FRAGMENT_PRECISION_HIGH"); src.push("precision highp float;"); src.push("precision highp int;"); src.push("#else"); src.push("precision mediump float;"); src.push("precision mediump int;"); src.push("#endif"); if (scene.logarithmicDepthBufferEnabled) { src.push("in float isPerspective;"); src.push("uniform float logDepthBufFC;"); src.push("in float vFragDepth;"); } if (clipping) { src.push("in vec4 vWorldPosition;"); src.push("in float vFlags;"); for (var i = 0; i < sectionPlanesState.getNumAllocatedSectionPlanes(); i++) { src.push("uniform bool sectionPlaneActive" + i + ";"); src.push("uniform vec3 sectionPlanePos" + i + ";"); src.push("uniform vec3 sectionPlaneDir" + i + ";"); } } src.push("in vec3 vWorldNormal;"); src.push("out highp ivec4 outNormal;"); src.push("void main(void) {"); if (clipping) { src.push(" bool clippable = (int(vFlags) >> 16 & 0xF) == 1;"); src.push(" if (clippable) {"); src.push(" float dist = 0.0;"); for (let i = 0; i < sectionPlanesState.getNumAllocatedSectionPlanes(); i++) { src.push(" if (sectionPlaneActive" + i + ") {"); src.push(" dist += clamp(dot(-sectionPlaneDir" + i + ".xyz, vWorldPosition.xyz - sectionPlanePos" + i + ".xyz), 0.0, 1000.0);"); src.push(" }"); } src.push(" if (dist > 0.0) { discard; }"); src.push(" }"); } if (scene.logarithmicDepthBufferEnabled) { src.push(" gl_FragDepth = isPerspective == 0.0 ? gl_FragCoord.z : log2( vFragDepth ) * logDepthBufFC * 0.5;"); } src.push(` outNormal = ivec4(vWorldNormal * float(${math.MAX_INT}), 1.0);`); src.push("}"); return src; } } /** * @private */ class TrianglesOcclusionRenderer$1 extends TrianglesBatchingRenderer { _buildVertexShader() { const scene = this._scene; const clipping = scene._sectionPlanesState.getNumAllocatedSectionPlanes() > 0; const src = []; src.push("#version 300 es"); src.push("// Triangles batching occlusion vertex shader"); src.push("uniform int renderPass;"); src.push("in vec3 position;"); if (scene.entityOffsetsEnabled) { src.push("in vec3 offset;"); } src.push("in vec4 color;"); src.push("in float flags;"); this._addMatricesUniformBlockLines(src); if (clipping) { src.push("out vec4 vWorldPosition;"); src.push("out float vFlags;"); } src.push("void main(void) {"); // colorFlag = NOT_RENDERED | COLOR_OPAQUE | COLOR_TRANSPARENT // renderPass = COLOR_OPAQUE // Only opaque objects can be occluders src.push(`int colorFlag = int(flags) & 0xF;`); src.push(`if (colorFlag != renderPass) {`); src.push(" gl_Position = vec4(0.0, 0.0, 0.0, 0.0);"); // Cull vertex src.push(" } else {"); src.push(" vec4 worldPosition = worldMatrix * (positionsDecodeMatrix * vec4(position, 1.0)); "); if (scene.entityOffsetsEnabled) { src.push(" worldPosition.xyz = worldPosition.xyz + offset;"); } src.push(" vec4 viewPosition = viewMatrix * worldPosition; "); if (clipping) { src.push(" vWorldPosition = worldPosition;"); src.push(" vFlags = flags;"); } src.push("vec4 clipPos = projMatrix * viewPosition;"); src.push("gl_Position = clipPos;"); src.push(" }"); src.push("}"); return src; } _buildFragmentShader() { const scene = this._scene; const sectionPlanesState = scene._sectionPlanesState; const clipping = sectionPlanesState.getNumAllocatedSectionPlanes() > 0; const src = []; src.push("#version 300 es"); src.push("// Triangles batching occlusion fragment shader"); src.push("#ifdef GL_FRAGMENT_PRECISION_HIGH"); src.push("precision highp float;"); src.push("precision highp int;"); src.push("#else"); src.push("precision mediump float;"); src.push("precision mediump int;"); src.push("#endif"); if (clipping) { src.push("in vec4 vWorldPosition;"); src.push("in float vFlags;"); for (let i = 0; i < sectionPlanesState.getNumAllocatedSectionPlanes(); i++) { src.push("uniform bool sectionPlaneActive" + i + ";"); src.push("uniform vec3 sectionPlanePos" + i + ";"); src.push("uniform vec3 sectionPlaneDir" + i + ";"); } } src.push("out vec4 outColor;"); src.push("void main(void) {"); if (clipping) { src.push(" bool clippable = (int(vFlags) >> 16 & 0xF) == 1;"); src.push(" if (clippable) {"); src.push(" float dist = 0.0;"); for (let i = 0; i < sectionPlanesState.getNumAllocatedSectionPlanes(); i++) { src.push(" if (sectionPlaneActive" + i + ") {"); src.push(" dist += clamp(dot(-sectionPlaneDir" + i + ".xyz, vWorldPosition.xyz - sectionPlanePos" + i + ".xyz), 0.0, 1000.0);"); src.push(" }"); } src.push(" if (dist > 0.0) { discard; }"); src.push(" }"); } src.push(" outColor = vec4(0.0, 0.0, 1.0, 1.0); "); // Occluders are blue src.push("}"); return src; } } /** * @private */ class TrianglesDepthRenderer$1 extends TrianglesBatchingRenderer { _buildVertexShader() { const scene = this._scene; const clipping = scene._sectionPlanesState.getNumAllocatedSectionPlanes() > 0; const src = []; src.push("#version 300 es"); src.push("// Triangles batching depth vertex shader"); src.push("uniform int renderPass;"); src.push("in vec3 position;"); if (scene.entityOffsetsEnabled) { src.push("in vec3 offset;"); } src.push("in float flags;"); this._addMatricesUniformBlockLines(src); if (scene.logarithmicDepthBufferEnabled) { src.push("uniform float logDepthBufFC;"); src.push("out float vFragDepth;"); src.push("bool isPerspectiveMatrix(mat4 m) {"); src.push(" return (m[2][3] == - 1.0);"); src.push("}"); src.push("out float isPerspective;"); } if (clipping) { src.push("out vec4 vWorldPosition;"); src.push("out float vFlags;"); } src.push("out vec2 vHighPrecisionZW;"); src.push("void main(void) {"); // colorFlag = NOT_RENDERED | COLOR_OPAQUE | COLOR_TRANSPARENT // renderPass = COLOR_OPAQUE src.push(`int colorFlag = int(flags) & 0xF;`); src.push(`if (colorFlag != renderPass) {`); src.push(" gl_Position = vec4(0.0, 0.0, 0.0, 0.0);"); // Cull vertex src.push(" } else {"); src.push(" vec4 worldPosition = worldMatrix * (positionsDecodeMatrix * vec4(position, 1.0)); "); if (scene.entityOffsetsEnabled) { src.push(" worldPosition.xyz = worldPosition.xyz + offset;"); } src.push(" vec4 viewPosition = viewMatrix * worldPosition; "); if (clipping) { src.push(" vWorldPosition = worldPosition;"); src.push(" vFlags = flags;"); } src.push("vec4 clipPos = projMatrix * viewPosition;"); if (scene.logarithmicDepthBufferEnabled) { src.push("vFragDepth = 1.0 + clipPos.w;"); src.push("isPerspective = float (isPerspectiveMatrix(projMatrix));"); } src.push("gl_Position = clipPos;"); src.push("vHighPrecisionZW = gl_Position.zw;"); src.push(" }"); src.push("}"); return src; } _buildFragmentShader() { const scene = this._scene; const sectionPlanesState = scene._sectionPlanesState; const clipping = (sectionPlanesState.getNumAllocatedSectionPlanes() > 0); const src = []; src.push("#version 300 es"); src.push("// Triangles batching depth fragment shader"); src.push("precision highp float;"); src.push("precision highp int;"); if (scene.logarithmicDepthBufferEnabled) { src.push("in float isPerspective;"); src.push("uniform float logDepthBufFC;"); src.push("in float vFragDepth;"); } if (clipping) { src.push("in vec4 vWorldPosition;"); src.push("in float vFlags;"); for (let i = 0; i < sectionPlanesState.getNumAllocatedSectionPlanes(); i++) { src.push("uniform bool sectionPlaneActive" + i + ";"); src.push("uniform vec3 sectionPlanePos" + i + ";"); src.push("uniform vec3 sectionPlaneDir" + i + ";"); } } src.push("in vec2 vHighPrecisionZW;"); src.push("out vec4 outColor;"); src.push("void main(void) {"); if (clipping) { src.push(" bool clippable = (int(vFlags) >> 16 & 0xF) == 1;"); src.push(" if (clippable) {"); src.push(" float dist = 0.0;"); for (var i = 0; i < sectionPlanesState.getNumAllocatedSectionPlanes(); i++) { src.push(" if (sectionPlaneActive" + i + ") {"); src.push(" dist += clamp(dot(-sectionPlaneDir" + i + ".xyz, vWorldPosition.xyz - sectionPlanePos" + i + ".xyz), 0.0, 1000.0);"); src.push(" }"); } src.push(" if (dist > 0.0) { discard; }"); src.push(" }"); } if (scene.logarithmicDepthBufferEnabled) { src.push(" gl_FragDepth = isPerspective == 0.0 ? gl_FragCoord.z : log2( vFragDepth ) * logDepthBufFC * 0.5;"); } src.push("float fragCoordZ = 0.5 * vHighPrecisionZW[0] / vHighPrecisionZW[1] + 0.5;"); src.push(" outColor = vec4(vec3(1.0 - fragCoordZ), 1.0); "); src.push("}"); return src; } } /** * @private */ class TrianglesNormalsRenderer$1 extends TrianglesBatchingRenderer { _buildVertexShader() { const scene = this._scene; const clipping = scene._sectionPlanesState.getNumAllocatedSectionPlanes() > 0; const src = []; src.push("#version 300 es"); src.push("// Batched geometry normals vertex shader"); src.push("uniform int renderPass;"); src.push("in vec3 position;"); if (scene.entityOffsetsEnabled) { src.push("in vec3 offset;"); } src.push("in vec3 normal;"); src.push("in vec4 color;"); src.push("in float flags;"); this._addMatricesUniformBlockLines(src, true); if (scene.logarithmicDepthBufferEnabled) { src.push("uniform float logDepthBufFC;"); src.push("out float vFragDepth;"); src.push("bool isPerspectiveMatrix(mat4 m) {"); src.push(" return (m[2][3] == - 1.0);"); src.push("}"); src.push("out float isPerspective;"); } src.push("vec3 octDecode(vec2 oct) {"); src.push(" vec3 v = vec3(oct.xy, 1.0 - abs(oct.x) - abs(oct.y));"); src.push(" if (v.z < 0.0) {"); src.push(" v.xy = (1.0 - abs(v.yx)) * vec2(v.x >= 0.0 ? 1.0 : -1.0, v.y >= 0.0 ? 1.0 : -1.0);"); src.push(" }"); src.push(" return normalize(v);"); src.push("}"); if (clipping) { src.push("out vec4 vWorldPosition;"); src.push("out float vFlags;"); } src.push("out vec3 vViewNormal;"); src.push("void main(void) {"); // colorFlag = NOT_RENDERED | COLOR_OPAQUE | COLOR_TRANSPARENT // renderPass = COLOR_OPAQUE src.push(`int colorFlag = int(flags) & 0xF;`); src.push(`if (colorFlag != renderPass) {`); src.push(" gl_Position = vec4(0.0, 0.0, 0.0, 0.0);"); src.push(" } else {"); src.push(" vec4 worldPosition = worldMatrix * (positionsDecodeMatrix * vec4(position, 1.0)); "); if (scene.entityOffsetsEnabled) { src.push(" worldPosition.xyz = worldPosition.xyz + offset;"); } src.push(" vec4 viewPosition = viewMatrix * worldPosition; "); src.push(" vec4 worldNormal = worldNormalMatrix * vec4(octDecode(normal.xy), 0.0); "); src.push(" vec3 viewNormal = normalize((viewNormalMatrix * worldNormal).xyz);"); if (clipping) { src.push(" vWorldPosition = worldPosition;"); src.push(" vFlags = flags;"); } src.push(" vViewNormal = viewNormal;"); src.push("vec4 clipPos = projMatrix * viewPosition;"); if (scene.logarithmicDepthBufferEnabled) { src.push("vFragDepth = 1.0 + clipPos.w;"); src.push("isPerspective = float (isPerspectiveMatrix(projMatrix));"); } src.push("gl_Position = clipPos;"); src.push(" }"); src.push("}"); return src; } _buildFragmentShader() { const scene = this._scene; const sectionPlanesState = scene._sectionPlanesState; const clipping = (sectionPlanesState.getNumAllocatedSectionPlanes() > 0); const src = []; src.push("#version 300 es"); src.push("// Batched geometry normals fragment shader"); src.push("#ifdef GL_FRAGMENT_PRECISION_HIGH"); src.push("precision highp float;"); src.push("precision highp int;"); src.push("#else"); src.push("precision mediump float;"); src.push("precision mediump int;"); src.push("#endif"); if (scene.logarithmicDepthBufferEnabled) { src.push("in float isPerspective;"); src.push("uniform float logDepthBufFC;"); src.push("in float vFragDepth;"); } if (clipping) { src.push("in vec4 vWorldPosition;"); src.push("in float vFlags;"); for (let i = 0; i < sectionPlanesState.getNumAllocatedSectionPlanes(); i++) { src.push("uniform bool sectionPlaneActive" + i + ";"); src.push("uniform vec3 sectionPlanePos" + i + ";"); src.push("uniform vec3 sectionPlaneDir" + i + ";"); } } src.push("in vec3 vViewNormal;"); src.push("vec3 packNormalToRGB( const in vec3 normal ) {"); src.push(" return normalize( normal ) * 0.5 + 0.5;"); src.push("}"); src.push("out vec4 outColor;"); src.push("void main(void) {"); if (clipping) { src.push(" bool clippable = (int(vFlags) >> 16 & 0xF) == 1;"); src.push(" if (clippable) {"); src.push(" float dist = 0.0;"); for (var i = 0; i < sectionPlanesState.getNumAllocatedSectionPlanes(); i++) { src.push(" if (sectionPlaneActive" + i + ") {"); src.push(" dist += clamp(dot(-sectionPlaneDir" + i + ".xyz, vWorldPosition.xyz - sectionPlanePos" + i + ".xyz), 0.0, 1000.0);"); src.push(" }"); } src.push(" if (dist > 0.0) { discard; }"); src.push(" }"); } if (scene.logarithmicDepthBufferEnabled) { src.push(" gl_FragDepth = isPerspective == 0.0 ? gl_FragCoord.z : log2( vFragDepth ) * logDepthBufFC * 0.5;"); } src.push(" outColor = vec4(packNormalToRGB(vViewNormal), 1.0); "); src.push("}"); return src; } } /** * Renders BatchingLayer fragment depths to a shadow map. * * @private */ class TrianglesShadowRenderer$1 extends TrianglesBatchingRenderer { _buildVertexShader() { const scene = this._scene; const clipping = scene._sectionPlanesState.getNumAllocatedSectionPlanes() > 0; const src = []; src.push("#version 300 es"); src.push("// Batched geometry shadow vertex shader"); src.push("in vec3 position;"); if (scene.entityOffsetsEnabled) { src.push("in vec3 offset;"); } src.push("in vec4 color;"); src.push("in float flags;"); src.push("uniform mat4 shadowViewMatrix;"); src.push("uniform mat4 shadowProjMatrix;"); this._addMatricesUniformBlockLines(src); if (clipping) { src.push("out vec4 vWorldPosition;"); src.push("out float vFlags;"); } src.push("out vec4 vViewPosition;"); src.push("out vec4 outColor;"); src.push("void main(void) {"); src.push(` int colorFlag = int(flags) & 0xF;`); src.push(" bool visible = (colorFlag > 0);"); src.push(" bool transparent = ((float(color.a) / 255.0) < 1.0);"); src.push(" if (!visible || transparent) {"); src.push(" gl_Position = vec4(0.0, 0.0, 0.0, 0.0);"); src.push(" } else {"); src.push(" vec4 worldPosition = positionsDecodeMatrix * vec4(position, 1.0); "); if (scene.entityOffsetsEnabled) { src.push(" worldPosition.xyz = worldPosition.xyz + offset;"); } src.push(" vec4 viewPosition = shadowViewMatrix * worldPosition; "); if (clipping) { src.push(" vWorldPosition = worldPosition;"); src.push(" vFlags = flags;"); } src.push(" vViewPosition = viewPosition;"); src.push(" gl_Position = shadowProjMatrix * viewPosition;"); src.push(" }"); src.push("}"); return src; } _buildFragmentShader() { const scene = this._scene; const sectionPlanesState = scene._sectionPlanesState; const clipping = (sectionPlanesState.getNumAllocatedSectionPlanes() > 0); const src = []; src.push("#version 300 es"); src.push("// Batched geometry shadow fragment shader"); src.push("#ifdef GL_FRAGMENT_PRECISION_HIGH"); src.push("precision highp float;"); src.push("precision highp int;"); src.push("#else"); src.push("precision mediump float;"); src.push("precision mediump int;"); src.push("#endif"); if (clipping) { src.push("in vec4 vWorldPosition;"); src.push("in float vFlags;"); for (let i = 0; i < sectionPlanesState.getNumAllocatedSectionPlanes(); i++) { src.push("uniform bool sectionPlaneActive" + i + ";"); src.push("uniform vec3 sectionPlanePos" + i + ";"); src.push("uniform vec3 sectionPlaneDir" + i + ";"); } } src.push("in vec4 vViewPosition;"); src.push("vec4 encodeFloat( const in float v ) {"); src.push(" const vec4 bitShift = vec4(256 * 256 * 256, 256 * 256, 256, 1.0);"); src.push(" const vec4 bitMask = vec4(0, 1.0 / 256.0, 1.0 / 256.0, 1.0 / 256.0);"); src.push(" vec4 comp = fract(v * bitShift);"); src.push(" comp -= comp.xxyz * bitMask;"); src.push(" return comp;"); src.push("}"); src.push("out vec4 outColor;"); src.push("void main(void) {"); if (clipping) { src.push(" bool clippable = (int(vFlags) >> 16 & 0xF) == 1;"); src.push(" if (clippable) {"); src.push(" float dist = 0.0;"); for (var i = 0; i < sectionPlanesState.getNumAllocatedSectionPlanes(); i++) { src.push(" if (sectionPlaneActive" + i + ") {"); src.push(" dist += clamp(dot(-sectionPlaneDir" + i + ".xyz, vWorldPosition.xyz - sectionPlanePos" + i + ".xyz), 0.0, 1000.0);"); src.push(" }"); } src.push(" if (dist > 0.0) { discard; }"); src.push(" }"); } src.push(" outColor = encodeFloat( gl_FragCoord.z); "); src.push("}"); return src; } } // const TEXTURE_DECODE_FUNCS = {}; // TEXTURE_DECODE_FUNCS[LinearEncoding] = "linearToLinear"; // TEXTURE_DECODE_FUNCS[sRGBEncoding] = "sRGBToLinear"; /** * @private */ class TrianglesPBRRenderer$1 extends TrianglesBatchingRenderer { _getHash() { const scene = this._scene; return [scene.gammaOutput, scene._lightsState.getHash(), scene._sectionPlanesState.getHash(), (this._withSAO ? "sao" : "nosao")].join(";"); } drawLayer(frameCtx, layer, renderPass) { super.drawLayer(frameCtx, layer, renderPass, { incrementDrawState: true }); } _buildVertexShader() { const scene = this._scene; const sectionPlanesState = scene._sectionPlanesState; const lightsState = scene._lightsState; const clipping = sectionPlanesState.getNumAllocatedSectionPlanes() > 0; const clippingCaps = sectionPlanesState.clippingCaps; const src = []; src.push("#version 300 es"); src.push("// Triangles batching quality draw vertex shader"); src.push("#ifdef GL_FRAGMENT_PRECISION_HIGH"); src.push("precision highp float;"); src.push("precision highp int;"); src.push("precision highp usampler2D;"); src.push("precision highp isampler2D;"); src.push("precision highp sampler2D;"); src.push("#else"); src.push("precision mediump float;"); src.push("precision mediump int;"); src.push("precision mediump usampler2D;"); src.push("precision mediump isampler2D;"); src.push("precision mediump sampler2D;"); src.push("#endif"); src.push("uniform int renderPass;"); src.push("in vec3 position;"); src.push("in vec3 normal;"); src.push("in vec4 color;"); src.push("in vec2 uv;"); src.push("in vec2 metallicRoughness;"); src.push("in float flags;"); if (scene.entityOffsetsEnabled) { src.push("in vec3 offset;"); } this._addMatricesUniformBlockLines(src, true); src.push("uniform mat3 uvDecodeMatrix;"); if (scene.logarithmicDepthBufferEnabled) { src.push("uniform float logDepthBufFC;"); src.push("out float vFragDepth;"); src.push("bool isPerspectiveMatrix(mat4 m) {"); src.push(" return (m[2][3] == - 1.0);"); src.push("}"); src.push("out float isPerspective;"); } src.push("vec3 octDecode(vec2 oct) {"); src.push(" vec3 v = vec3(oct.xy, 1.0 - abs(oct.x) - abs(oct.y));"); src.push(" if (v.z < 0.0) {"); src.push(" v.xy = (1.0 - abs(v.yx)) * vec2(v.x >= 0.0 ? 1.0 : -1.0, v.y >= 0.0 ? 1.0 : -1.0);"); src.push(" }"); src.push(" return normalize(v);"); src.push("}"); src.push("out vec4 vViewPosition;"); src.push("out vec3 vViewNormal;"); src.push("out vec4 vColor;"); src.push("out vec2 vUV;"); src.push("out vec2 vMetallicRoughness;"); if (lightsState.lightMaps.length > 0) { src.push("out vec3 vWorldNormal;"); } if (clipping) { src.push("out vec4 vWorldPosition;"); src.push("out float vFlags;"); if (clippingCaps) { src.push("out vec4 vClipPosition;"); } } src.push("void main(void) {"); // colorFlag = NOT_RENDERED | COLOR_OPAQUE | COLOR_TRANSPARENT // renderPass = COLOR_OPAQUE src.push(`int colorFlag = int(flags) & 0xF;`); src.push(`if (colorFlag != renderPass) {`); src.push(" gl_Position = vec4(0.0, 0.0, 0.0, 0.0);"); // Cull vertex src.push("} else {"); src.push("vec4 worldPosition = worldMatrix * (positionsDecodeMatrix * vec4(position, 1.0)); "); if (scene.entityOffsetsEnabled) { src.push("worldPosition.xyz = worldPosition.xyz + offset;"); } src.push("vec4 viewPosition = viewMatrix * worldPosition; "); src.push("vec4 worldNormal = worldNormalMatrix * vec4(octDecode(normal.xy), 0.0); "); src.push("vec3 viewNormal = normalize((viewNormalMatrix * worldNormal).xyz);"); src.push("vec4 clipPos = projMatrix * viewPosition;"); if (scene.logarithmicDepthBufferEnabled) { src.push("isPerspective = float (isPerspectiveMatrix(projMatrix));"); src.push("vFragDepth = 1.0 + clipPos.w;"); } if (clipping) { src.push("vWorldPosition = worldPosition;"); src.push("vFlags = flags;"); if (clippingCaps) { src.push("vClipPosition = clipPos;"); } } src.push("vViewPosition = viewPosition;"); src.push("vViewNormal = viewNormal;"); src.push("vColor = color;"); src.push("vUV = (uvDecodeMatrix * vec3(uv, 1.0)).xy;"); src.push("vMetallicRoughness = metallicRoughness;"); if (lightsState.lightMaps.length > 0) { src.push("vWorldNormal = worldNormal.xyz;"); } src.push("gl_Position = clipPos;"); src.push("}"); src.push("}"); return src; } _buildFragmentShader() { const scene = this._scene; const gammaOutput = scene.gammaOutput; // If set, then it expects that all textures and colors need to be outputted in premultiplied gamma. Default is false. const sectionPlanesState = scene._sectionPlanesState; const lightsState = scene._lightsState; const clipping = sectionPlanesState.getNumAllocatedSectionPlanes() > 0; const clippingCaps = sectionPlanesState.clippingCaps; const src = []; src.push('#version 300 es'); src.push("// Triangles batching quality draw fragment shader"); src.push("#ifdef GL_FRAGMENT_PRECISION_HIGH"); src.push("precision highp float;"); src.push("precision highp int;"); src.push("#else"); src.push("precision mediump float;"); src.push("precision mediump int;"); src.push("#endif"); if (scene.logarithmicDepthBufferEnabled) { src.push("in float isPerspective;"); src.push("uniform float logDepthBufFC;"); src.push("in float vFragDepth;"); } src.push("uniform sampler2D uColorMap;"); src.push("uniform sampler2D uMetallicRoughMap;"); src.push("uniform sampler2D uEmissiveMap;"); src.push("uniform sampler2D uNormalMap;"); src.push("uniform sampler2D uAOMap;"); src.push("in vec4 vViewPosition;"); src.push("in vec3 vViewNormal;"); src.push("in vec4 vColor;"); src.push("in vec2 vUV;"); src.push("in vec2 vMetallicRoughness;"); if (lightsState.lightMaps.length > 0) { src.push("in vec3 vWorldNormal;"); } this._addMatricesUniformBlockLines(src, true); if (lightsState.reflectionMaps.length > 0) { src.push("uniform samplerCube reflectionMap;"); } if (lightsState.lightMaps.length > 0) { src.push("uniform samplerCube lightMap;"); } src.push("uniform vec4 lightAmbient;"); for (let i = 0, len = lightsState.lights.length; i < len; i++) { const light = lightsState.lights[i]; if (light.type === "ambient") { continue; } src.push("uniform vec4 lightColor" + i + ";"); if (light.type === "dir") { src.push("uniform vec3 lightDir" + i + ";"); } if (light.type === "point") { src.push("uniform vec3 lightPos" + i + ";"); } if (light.type === "spot") { src.push("uniform vec3 lightPos" + i + ";"); src.push("uniform vec3 lightDir" + i + ";"); } } if (this._withSAO) { src.push("uniform sampler2D uOcclusionTexture;"); src.push("uniform vec4 uSAOParams;"); src.push("const float packUpscale = 256. / 255.;"); src.push("const float unpackDownScale = 255. / 256.;"); src.push("const vec3 packFactors = vec3( 256. * 256. * 256., 256. * 256., 256. );"); src.push("const vec4 unPackFactors = unpackDownScale / vec4( packFactors, 1. );"); src.push("float unpackRGBToFloat( const in vec4 v ) {"); src.push(" return dot( v, unPackFactors );"); src.push("}"); } src.push("uniform float gammaFactor;"); src.push("vec4 linearToLinear( in vec4 value ) {"); src.push(" return value;"); src.push("}"); src.push("vec4 sRGBToLinear( in vec4 value ) {"); src.push(" return vec4( mix( pow( value.rgb * 0.9478672986 + vec3( 0.0521327014 ), vec3( 2.4 ) ), value.rgb * 0.0773993808, vec3( lessThanEqual( value.rgb, vec3( 0.04045 ) ) ) ), value.w );"); src.push("}"); src.push("vec4 gammaToLinear( in vec4 value) {"); src.push(" return vec4( pow( value.xyz, vec3( gammaFactor ) ), value.w );"); src.push("}"); if (gammaOutput) { src.push("vec4 linearToGamma( in vec4 value, in float gammaFactor ) {"); src.push(" return vec4( pow( value.xyz, vec3( 1.0 / gammaFactor ) ), value.w );"); src.push("}"); } if (clipping) { src.push("in vec4 vWorldPosition;"); src.push("in float vFlags;"); if (clippingCaps) { src.push("in vec4 vClipPosition;"); } for (let i = 0, len = sectionPlanesState.getNumAllocatedSectionPlanes(); i < len; i++) { src.push("uniform bool sectionPlaneActive" + i + ";"); src.push("uniform vec3 sectionPlanePos" + i + ";"); src.push("uniform vec3 sectionPlaneDir" + i + ";"); } } // CONSTANT DEFINITIONS src.push("#define PI 3.14159265359"); src.push("#define RECIPROCAL_PI 0.31830988618"); src.push("#define RECIPROCAL_PI2 0.15915494"); src.push("#define EPSILON 1e-6"); src.push("#define saturate(a) clamp( a, 0.0, 1.0 )"); // UTILITY DEFINITIONS src.push("vec3 perturbNormal2Arb( vec3 eye_pos, vec3 surf_norm, vec2 uv ) {"); src.push(" vec3 texel = texture( uNormalMap, uv ).xyz;"); src.push(" if (texel.x == 0.0 && texel.y == 0.0 && texel.z == 0.0) {"); src.push(" return surf_norm;"); src.push(" }"); src.push(" vec3 q0 = vec3( dFdx( eye_pos.x ), dFdx( eye_pos.y ), dFdx( eye_pos.z ) );"); src.push(" vec3 q1 = vec3( dFdy( eye_pos.x ), dFdy( eye_pos.y ), dFdy( eye_pos.z ) );"); src.push(" vec2 st0 = dFdx( uv.st );"); src.push(" vec2 st1 = dFdy( uv.st );"); src.push(" vec3 S = normalize( q0 * st1.t - q1 * st0.t );"); src.push(" vec3 T = normalize( -q0 * st1.s + q1 * st0.s );"); src.push(" vec3 N = normalize( surf_norm );"); src.push(" vec3 mapN = texel.xyz * 2.0 - 1.0;"); src.push(" mat3 tsn = mat3( S, T, N );"); //src.push(" mapN *= 3.0;"); src.push(" return normalize( tsn * mapN );"); src.push("}"); src.push("vec3 inverseTransformDirection(in vec3 dir, in mat4 matrix) {"); src.push(" return normalize( ( vec4( dir, 0.0 ) * matrix ).xyz );"); src.push("}"); // STRUCTURES src.push("struct IncidentLight {"); src.push(" vec3 color;"); src.push(" vec3 direction;"); src.push("};"); src.push("struct ReflectedLight {"); src.push(" vec3 diffuse;"); src.push(" vec3 specular;"); src.push("};"); src.push("struct Geometry {"); src.push(" vec3 position;"); src.push(" vec3 viewNormal;"); src.push(" vec3 worldNormal;"); src.push(" vec3 viewEyeDir;"); src.push("};"); src.push("struct Material {"); src.push(" vec3 diffuseColor;"); src.push(" float specularRoughness;"); src.push(" vec3 specularColor;"); src.push(" float shine;"); // Only used for Phong src.push("};"); // IRRADIANCE EVALUATION src.push("float GGXRoughnessToBlinnExponent(const in float ggxRoughness) {"); src.push(" float r = ggxRoughness + 0.0001;"); src.push(" return (2.0 / (r * r) - 2.0);"); src.push("}"); src.push("float getSpecularMIPLevel(const in float blinnShininessExponent, const in int maxMIPLevel) {"); src.push(" float maxMIPLevelScalar = float( maxMIPLevel );"); src.push(" float desiredMIPLevel = maxMIPLevelScalar - 0.79248 - 0.5 * log2( ( blinnShininessExponent * blinnShininessExponent ) + 1.0 );"); src.push(" return clamp( desiredMIPLevel, 0.0, maxMIPLevelScalar );"); src.push("}"); if (lightsState.reflectionMaps.length > 0) { src.push("vec3 getLightProbeIndirectRadiance(const in vec3 reflectVec, const in float blinnShininessExponent, const in int maxMIPLevel) {"); src.push(" float mipLevel = 0.5 * getSpecularMIPLevel(blinnShininessExponent, maxMIPLevel);"); //TODO: a random factor - fix this src.push(" vec3 envMapColor = sRGBToLinear(texture(reflectionMap, reflectVec, mipLevel)).rgb;"); src.push(" return envMapColor;"); src.push("}"); } // SPECULAR BRDF EVALUATION src.push("vec3 F_Schlick(const in vec3 specularColor, const in float dotLH) {"); src.push(" float fresnel = exp2( ( -5.55473 * dotLH - 6.98316 ) * dotLH );"); src.push(" return ( 1.0 - specularColor ) * fresnel + specularColor;"); src.push("}"); src.push("float G_GGX_Smith(const in float alpha, const in float dotNL, const in float dotNV) {"); src.push(" float a2 = ( alpha * alpha );"); src.push(" float gl = dotNL + sqrt( a2 + ( 1.0 - a2 ) * ( dotNL * dotNL ) );"); src.push(" float gv = dotNV + sqrt( a2 + ( 1.0 - a2 ) * ( dotNV * dotNV ) );"); src.push(" return 1.0 / ( gl * gv );"); src.push("}"); src.push("float G_GGX_SmithCorrelated(const in float alpha, const in float dotNL, const in float dotNV) {"); src.push(" float a2 = ( alpha * alpha );"); src.push(" float gv = dotNL * sqrt( a2 + ( 1.0 - a2 ) * ( dotNV * dotNV ) );"); src.push(" float gl = dotNV * sqrt( a2 + ( 1.0 - a2 ) * ( dotNL * dotNL ) );"); src.push(" return 0.5 / max( gv + gl, EPSILON );"); src.push("}"); src.push("float D_GGX(const in float alpha, const in float dotNH) {"); src.push(" float a2 = ( alpha * alpha );"); src.push(" float denom = ( dotNH * dotNH) * ( a2 - 1.0 ) + 1.0;"); src.push(" return RECIPROCAL_PI * a2 / ( denom * denom);"); src.push("}"); src.push("vec3 BRDF_Specular_GGX(const in IncidentLight incidentLight, const in Geometry geometry, const in vec3 specularColor, const in float roughness) {"); src.push(" float alpha = ( roughness * roughness );"); src.push(" vec3 halfDir = normalize( incidentLight.direction + geometry.viewEyeDir );"); src.push(" float dotNL = saturate( dot( geometry.viewNormal, incidentLight.direction ) );"); src.push(" float dotNV = saturate( dot( geometry.viewNormal, geometry.viewEyeDir ) );"); src.push(" float dotNH = saturate( dot( geometry.viewNormal, halfDir ) );"); src.push(" float dotLH = saturate( dot( incidentLight.direction, halfDir ) );"); src.push(" vec3 F = F_Schlick( specularColor, dotLH );"); src.push(" float G = G_GGX_SmithCorrelated( alpha, dotNL, dotNV );"); src.push(" float D = D_GGX( alpha, dotNH );"); src.push(" return F * (G * D);"); src.push("}"); src.push("vec3 BRDF_Specular_GGX_Environment(const in Geometry geometry, const in vec3 specularColor, const in float roughness) {"); src.push(" float dotNV = saturate(dot(geometry.viewNormal, geometry.viewEyeDir));"); src.push(" const vec4 c0 = vec4( -1, -0.0275, -0.572, 0.022);"); src.push(" const vec4 c1 = vec4( 1, 0.0425, 1.04, -0.04);"); src.push(" vec4 r = roughness * c0 + c1;"); src.push(" float a004 = min(r.x * r.x, exp2(-9.28 * dotNV)) * r.x + r.y;"); src.push(" vec2 AB = vec2(-1.04, 1.04) * a004 + r.zw;"); src.push(" return specularColor * AB.x + AB.y;"); src.push("}"); if (lightsState.lightMaps.length > 0 || lightsState.reflectionMaps.length > 0) { src.push("void computePBRLightMapping(const in Geometry geometry, const in Material material, inout ReflectedLight reflectedLight) {"); if (lightsState.lightMaps.length > 0) { src.push(" vec3 irradiance = sRGBToLinear(texture(lightMap, geometry.worldNormal)).rgb;"); src.push(" irradiance *= PI;"); src.push(" vec3 diffuseBRDFContrib = (RECIPROCAL_PI * material.diffuseColor);"); src.push(" reflectedLight.diffuse += irradiance * diffuseBRDFContrib;"); } if (lightsState.reflectionMaps.length > 0) { src.push(" vec3 reflectVec = reflect(geometry.viewEyeDir, geometry.viewNormal);"); src.push(" reflectVec = inverseTransformDirection(reflectVec, viewMatrix);"); src.push(" float blinnExpFromRoughness = GGXRoughnessToBlinnExponent(material.specularRoughness);"); src.push(" vec3 radiance = getLightProbeIndirectRadiance(reflectVec, blinnExpFromRoughness, 8);"); src.push(" vec3 specularBRDFContrib = BRDF_Specular_GGX_Environment(geometry, material.specularColor, material.specularRoughness);"); src.push(" reflectedLight.specular += radiance * specularBRDFContrib;"); } src.push("}"); } // MAIN LIGHTING COMPUTATION FUNCTION src.push("void computePBRLighting(const in IncidentLight incidentLight, const in Geometry geometry, const in Material material, inout ReflectedLight reflectedLight) {"); src.push(" float dotNL = saturate(dot(geometry.viewNormal, incidentLight.direction));"); src.push(" vec3 irradiance = dotNL * incidentLight.color * PI;"); src.push(" reflectedLight.diffuse += irradiance * (RECIPROCAL_PI * material.diffuseColor);"); src.push(" reflectedLight.specular += irradiance * BRDF_Specular_GGX(incidentLight, geometry, material.specularColor, material.specularRoughness);"); src.push("}"); src.push("out vec4 outColor;"); src.push("void main(void) {"); if (clipping) { src.push(" bool clippable = (int(vFlags) >> 16 & 0xF) == 1;"); src.push(" if (clippable) {"); src.push(" float dist = 0.0;"); for (let i = 0, len = sectionPlanesState.getNumAllocatedSectionPlanes(); i < len; i++) { src.push("if (sectionPlaneActive" + i + ") {"); src.push(" dist += clamp(dot(-sectionPlaneDir" + i + ".xyz, vWorldPosition.xyz - sectionPlanePos" + i + ".xyz), 0.0, 1000.0);"); src.push("}"); } if (clippingCaps) { src.push(" if (dist > (0.002 * vClipPosition.w)) {"); src.push(" discard;"); src.push(" }"); src.push(" if (dist > 0.0) { "); src.push(" outColor=vec4(1.0, 0.0, 0.0, 1.0);"); if (scene.logarithmicDepthBufferEnabled) { src.push(" gl_FragDepth = log2( vFragDepth ) * logDepthBufFC * 0.5;"); } src.push(" return;"); src.push("}"); } else { src.push(" if (dist > 0.0) { "); src.push(" discard;"); src.push(" }"); } src.push("}"); } src.push("IncidentLight light;"); src.push("Material material;"); src.push("Geometry geometry;"); src.push("ReflectedLight reflectedLight = ReflectedLight(vec3(0.0,0.0,0.0), vec3(0.0,0.0,0.0));"); src.push("vec3 rgb = (vec3(float(vColor.r) / 255.0, float(vColor.g) / 255.0, float(vColor.b) / 255.0));"); src.push("float opacity = float(vColor.a) / 255.0;"); src.push("vec3 baseColor = rgb;"); src.push("float specularF0 = 1.0;"); src.push("float metallic = float(vMetallicRoughness.r) / 255.0;"); src.push("float roughness = float(vMetallicRoughness.g) / 255.0;"); src.push("float dielectricSpecular = 0.16 * specularF0 * specularF0;"); src.push("vec4 colorTexel = sRGBToLinear(texture(uColorMap, vUV));"); src.push("baseColor *= colorTexel.rgb;"); // src.push("opacity *= colorTexel.a;"); src.push("vec3 metalRoughTexel = texture(uMetallicRoughMap, vUV).rgb;"); src.push("metallic *= metalRoughTexel.b;"); src.push("roughness *= metalRoughTexel.g;"); src.push("vec3 viewNormal = perturbNormal2Arb(vViewPosition.xyz, normalize(vViewNormal), vUV );"); src.push("material.diffuseColor = baseColor * (1.0 - dielectricSpecular) * (1.0 - metallic);"); src.push("material.specularRoughness = clamp(roughness, 0.04, 1.0);"); src.push("material.specularColor = mix(vec3(dielectricSpecular), baseColor, metallic);"); src.push("geometry.position = vViewPosition.xyz;"); src.push("geometry.viewNormal = -normalize(viewNormal);"); src.push("geometry.viewEyeDir = normalize(vViewPosition.xyz);"); if (lightsState.lightMaps.length > 0) { src.push("geometry.worldNormal = normalize(vWorldNormal);"); } if (lightsState.lightMaps.length > 0 || lightsState.reflectionMaps.length > 0) { src.push("computePBRLightMapping(geometry, material, reflectedLight);"); } for (let i = 0, len = lightsState.lights.length; i < len; i++) { const light = lightsState.lights[i]; if (light.type === "ambient") { continue; } if (light.type === "dir") { if (light.space === "view") { src.push("light.direction = normalize(lightDir" + i + ");"); } else { src.push("light.direction = normalize((viewMatrix * vec4(lightDir" + i + ", 0.0)).xyz);"); } } else if (light.type === "point") { if (light.space === "view") { src.push("light.direction = normalize(lightPos" + i + " - vViewPosition.xyz);"); } else { src.push("light.direction = normalize((viewMatrix * vec4(lightPos" + i + ", 0.0)).xyz);"); } } else if (light.type === "spot") { if (light.space === "view") { src.push("light.direction = normalize(lightDir" + i + ");"); } else { src.push("light.direction = normalize((viewMatrix * vec4(lightDir" + i + ", 0.0)).xyz);"); } } else { continue; } src.push("light.color = lightColor" + i + ".rgb * lightColor" + i + ".a;"); // a is intensity src.push("computePBRLighting(light, geometry, material, reflectedLight);"); } src.push("vec3 emissiveColor = sRGBToLinear(texture(uEmissiveMap, vUV)).rgb;"); // TODO: correct gamma function src.push("float aoFactor = texture(uAOMap, vUV).r;"); src.push("vec3 outgoingLight = (lightAmbient.rgb * lightAmbient.a * baseColor * opacity * rgb) + (reflectedLight.diffuse) + (reflectedLight.specular) + emissiveColor;"); src.push("vec4 fragColor;"); if (this._withSAO) { // Doing SAO blend in the main solid fill draw shader just so that edge lines can be drawn over the top // Would be more efficient to defer this, then render lines later, using same depth buffer for Z-reject src.push(" float viewportWidth = uSAOParams[0];"); src.push(" float viewportHeight = uSAOParams[1];"); src.push(" float blendCutoff = uSAOParams[2];"); src.push(" float blendFactor = uSAOParams[3];"); src.push(" vec2 uv = vec2(gl_FragCoord.x / viewportWidth, gl_FragCoord.y / viewportHeight);"); src.push(" float ambient = smoothstep(blendCutoff, 1.0, unpackRGBToFloat(texture(uOcclusionTexture, uv))) * blendFactor;"); src.push(" fragColor = vec4(outgoingLight.rgb * ambient * aoFactor, opacity);"); } else { src.push(" fragColor = vec4(outgoingLight.rgb * aoFactor, opacity);"); } if (gammaOutput) { src.push("fragColor = linearToGamma(fragColor, gammaFactor);"); } src.push("outColor = fragColor;"); if (scene.logarithmicDepthBufferEnabled) { src.push(" gl_FragDepth = isPerspective == 0.0 ? gl_FragCoord.z : log2( vFragDepth ) * logDepthBufFC * 0.5;"); } src.push("}"); return src; } } /** * @private */ class TrianglesPickNormalsFlatRenderer$1 extends TrianglesBatchingRenderer { _buildVertexShader() { const scene = this._scene; const clipping = scene._sectionPlanesState.getNumAllocatedSectionPlanes() > 0; const src = []; src.push("#version 300 es"); src.push("// Triangles batching pick flat normals vertex shader"); src.push("uniform int renderPass;"); src.push("in vec3 position;"); if (scene.entityOffsetsEnabled) { src.push("in vec3 offset;"); } src.push("in float flags;"); src.push("uniform bool pickInvisible;"); this._addMatricesUniformBlockLines(src); this._addRemapClipPosLines(src, 3); if (scene.logarithmicDepthBufferEnabled) { src.push("uniform float logDepthBufFC;"); src.push("out float vFragDepth;"); src.push("bool isPerspectiveMatrix(mat4 m) {"); src.push(" return (m[2][3] == - 1.0);"); src.push("}"); src.push("out float isPerspective;"); } src.push("out vec4 vWorldPosition;"); if (clipping) { src.push("out float vFlags;"); } src.push("void main(void) {"); // pickFlag = NOT_RENDERED | PICK // renderPass = PICK src.push(`int pickFlag = int(flags) >> 12 & 0xF;`); src.push(`if (pickFlag != renderPass) {`); src.push(" gl_Position = vec4(0.0, 0.0, 0.0, 0.0);"); // Cull vertex src.push(" } else {"); src.push(" vec4 worldPosition = worldMatrix * (positionsDecodeMatrix * vec4(position, 1.0)); "); if (scene.entityOffsetsEnabled) { src.push(" worldPosition.xyz = worldPosition.xyz + offset;"); } src.push(" vec4 viewPosition = viewMatrix * worldPosition; "); src.push(" vWorldPosition = worldPosition;"); if (clipping) { src.push(" vFlags = flags;"); } src.push("vec4 clipPos = projMatrix * viewPosition;"); if (scene.logarithmicDepthBufferEnabled) { src.push("vFragDepth = 1.0 + clipPos.w;"); src.push("isPerspective = float (isPerspectiveMatrix(projMatrix));"); } src.push("gl_Position = remapClipPos(clipPos);"); src.push(" }"); src.push("}"); return src; } _buildFragmentShader() { const scene = this._scene; const sectionPlanesState = scene._sectionPlanesState; const clipping = sectionPlanesState.getNumAllocatedSectionPlanes() > 0; const src = []; src.push("#version 300 es"); src.push("// Triangles batching pick flat normals fragment shader"); src.push("#ifdef GL_FRAGMENT_PRECISION_HIGH"); src.push("precision highp float;"); src.push("precision highp int;"); src.push("#else"); src.push("precision mediump float;"); src.push("precision mediump int;"); src.push("#endif"); if (scene.logarithmicDepthBufferEnabled) { src.push("in float isPerspective;"); src.push("uniform float logDepthBufFC;"); src.push("in float vFragDepth;"); } src.push("in vec4 vWorldPosition;"); if (clipping) { src.push("in float vFlags;"); for (var i = 0; i < sectionPlanesState.getNumAllocatedSectionPlanes(); i++) { src.push("uniform bool sectionPlaneActive" + i + ";"); src.push("uniform vec3 sectionPlanePos" + i + ";"); src.push("uniform vec3 sectionPlaneDir" + i + ";"); } } src.push("out highp ivec4 outNormal;"); src.push("void main(void) {"); if (clipping) { src.push(" bool clippable = (int(vFlags) >> 16 & 0xF) == 1;"); src.push(" if (clippable) {"); src.push(" float dist = 0.0;"); for (let i = 0; i < sectionPlanesState.getNumAllocatedSectionPlanes(); i++) { src.push(" if (sectionPlaneActive" + i + ") {"); src.push(" dist += clamp(dot(-sectionPlaneDir" + i + ".xyz, vWorldPosition.xyz - sectionPlanePos" + i + ".xyz), 0.0, 1000.0);"); src.push(" }"); } src.push(" if (dist > 0.0) { discard; }"); src.push(" }"); } if (scene.logarithmicDepthBufferEnabled) { src.push(" gl_FragDepth = isPerspective == 0.0 ? gl_FragCoord.z : log2( vFragDepth ) * logDepthBufFC * 0.5;"); } src.push(" vec3 xTangent = dFdx( vWorldPosition.xyz );"); src.push(" vec3 yTangent = dFdy( vWorldPosition.xyz );"); src.push(" vec3 worldNormal = normalize( cross( xTangent, yTangent ) );"); src.push(` outNormal = ivec4(worldNormal * float(${math.MAX_INT}), 1.0);`); src.push("}"); return src; } } /** * @private */ class TrianglesColorTextureRenderer$1 extends TrianglesBatchingRenderer { _getHash() { const scene = this._scene; return [scene.gammaOutput, scene._lightsState.getHash(), scene._sectionPlanesState.getHash(), (this._withSAO ? "sao" : "nosao")].join(";"); } drawLayer(frameCtx, layer, renderPass) { super.drawLayer(frameCtx, layer, renderPass, { incrementDrawState: true }); } _buildVertexShader() { const scene = this._scene; const sectionPlanesState = scene._sectionPlanesState; const clipping = sectionPlanesState.getNumAllocatedSectionPlanes() > 0; const src = []; src.push("#version 300 es"); src.push("// Triangles batching color texture vertex shader"); src.push("uniform int renderPass;"); src.push("in vec3 position;"); src.push("in vec4 color;"); src.push("in vec2 uv;"); src.push("in float flags;"); if (scene.entityOffsetsEnabled) { src.push("in vec3 offset;"); } this._addMatricesUniformBlockLines(src); src.push("uniform mat3 uvDecodeMatrix;"); if (scene.logarithmicDepthBufferEnabled) { src.push("uniform float logDepthBufFC;"); src.push("out float vFragDepth;"); src.push("bool isPerspectiveMatrix(mat4 m) {"); src.push(" return (m[2][3] == - 1.0);"); src.push("}"); src.push("out float isPerspective;"); } if (clipping) { src.push("out vec4 vWorldPosition;"); src.push("out float vFlags;"); } src.push("out vec4 vViewPosition;"); src.push("out vec4 vColor;"); src.push("out vec2 vUV;"); src.push("void main(void) {"); // colorFlag = NOT_RENDERED | COLOR_OPAQUE | COLOR_TRANSPARENT // renderPass = COLOR_OPAQUE src.push(`int colorFlag = int(flags) & 0xF;`); src.push(`if (colorFlag != renderPass) {`); src.push(" gl_Position = vec4(0.0, 0.0, 0.0, 0.0);"); // Cull vertex src.push("} else {"); src.push("vec4 worldPosition = worldMatrix * (positionsDecodeMatrix * vec4(position, 1.0)); "); if (scene.entityOffsetsEnabled) { src.push("worldPosition.xyz = worldPosition.xyz + offset;"); } src.push("vec4 viewPosition = viewMatrix * worldPosition; "); src.push("vViewPosition = viewPosition;"); src.push("vColor = vec4(float(color.r) / 255.0, float(color.g) / 255.0, float(color.b) / 255.0, float(color.a) / 255.0);"); src.push("vUV = (uvDecodeMatrix * vec3(uv, 1.0)).xy;"); src.push("vec4 clipPos = projMatrix * viewPosition;"); if (scene.logarithmicDepthBufferEnabled) { src.push("vFragDepth = 1.0 + clipPos.w;"); src.push("isPerspective = float (isPerspectiveMatrix(projMatrix));"); } if (clipping) { src.push("vWorldPosition = worldPosition;"); src.push("vFlags = flags;"); } src.push("gl_Position = clipPos;"); src.push("}"); src.push("}"); return src; } _buildFragmentShader() { const scene = this._scene; const gammaOutput = scene.gammaOutput; // If set, then it expects that all textures and colors need to be outputted in premultiplied gamma. Default is false. const lightsState = scene._lightsState; const sectionPlanesState = scene._sectionPlanesState; const clipping = sectionPlanesState.getNumAllocatedSectionPlanes() > 0; const useAlphaCutoff = this._useAlphaCutoff; const src = []; src.push("#version 300 es"); src.push("// Triangles batching color texture fragment shader"); src.push("#ifdef GL_FRAGMENT_PRECISION_HIGH"); src.push("precision highp float;"); src.push("precision highp int;"); src.push("#else"); src.push("precision mediump float;"); src.push("precision mediump int;"); src.push("#endif"); if (scene.logarithmicDepthBufferEnabled) { src.push("in float isPerspective;"); src.push("uniform float logDepthBufFC;"); src.push("in float vFragDepth;"); } src.push("uniform sampler2D uColorMap;"); if (this._withSAO) { src.push("uniform sampler2D uOcclusionTexture;"); src.push("uniform vec4 uSAOParams;"); src.push("const float packUpscale = 256. / 255.;"); src.push("const float unpackDownScale = 255. / 256.;"); src.push("const vec3 packFactors = vec3( 256. * 256. * 256., 256. * 256., 256. );"); src.push("const vec4 unPackFactors = unpackDownScale / vec4( packFactors, 1. );"); src.push("float unpackRGBToFloat( const in vec4 v ) {"); src.push(" return dot( v, unPackFactors );"); src.push("}"); } src.push("uniform float gammaFactor;"); src.push("vec4 linearToLinear( in vec4 value ) {"); src.push(" return value;"); src.push("}"); src.push("vec4 sRGBToLinear( in vec4 value ) {"); src.push(" return vec4( mix( pow( value.rgb * 0.9478672986 + vec3( 0.0521327014 ), vec3( 2.4 ) ), value.rgb * 0.0773993808, vec3( lessThanEqual( value.rgb, vec3( 0.04045 ) ) ) ), value.w );"); src.push("}"); src.push("vec4 gammaToLinear( in vec4 value) {"); src.push(" return vec4( pow( value.xyz, vec3( gammaFactor ) ), value.w );"); src.push("}"); if (gammaOutput) { src.push("vec4 linearToGamma( in vec4 value, in float gammaFactor ) {"); src.push(" return vec4( pow( value.xyz, vec3( 1.0 / gammaFactor ) ), value.w );"); src.push("}"); } if (clipping) { src.push("in vec4 vWorldPosition;"); src.push("in float vFlags;"); for (let i = 0, len = sectionPlanesState.getNumAllocatedSectionPlanes(); i < len; i++) { src.push("uniform bool sectionPlaneActive" + i + ";"); src.push("uniform vec3 sectionPlanePos" + i + ";"); src.push("uniform vec3 sectionPlaneDir" + i + ";"); } src.push("uniform float sliceThickness;"); src.push("uniform vec4 sliceColor;"); } this._addMatricesUniformBlockLines(src); src.push("uniform vec4 lightAmbient;"); for (let i = 0, len = lightsState.lights.length; i < len; i++) { const light = lightsState.lights[i]; if (light.type === "ambient") { continue; } src.push("uniform vec4 lightColor" + i + ";"); if (light.type === "dir") { src.push("uniform vec3 lightDir" + i + ";"); } if (light.type === "point") { src.push("uniform vec3 lightPos" + i + ";"); } if (light.type === "spot") { src.push("uniform vec3 lightPos" + i + ";"); src.push("uniform vec3 lightDir" + i + ";"); } } if (useAlphaCutoff) { src.push("uniform float materialAlphaCutoff;"); } src.push("in vec4 vViewPosition;"); src.push("in vec4 vColor;"); src.push("in vec2 vUV;"); src.push("out vec4 outColor;"); src.push("void main(void) {"); src.push(" vec4 newColor;"); src.push(" newColor = vColor;"); if (clipping) { src.push(" bool clippable = (int(vFlags) >> 16 & 0xF) == 1;"); src.push(" if (clippable) {"); src.push(" float dist = 0.0;"); for (let i = 0, len = sectionPlanesState.getNumAllocatedSectionPlanes(); i < len; i++) { src.push("if (sectionPlaneActive" + i + ") {"); src.push(" dist += clamp(dot(-sectionPlaneDir" + i + ".xyz, vWorldPosition.xyz - sectionPlanePos" + i + ".xyz), 0.0, 1000.0);"); src.push("}"); } src.push(" if (dist > sliceThickness) { "); src.push(" discard;"); src.push(" }"); src.push(" if (dist > 0.0) { "); src.push(" newColor = sliceColor;"); src.push(" }"); src.push("}"); } src.push("vec3 reflectedColor = vec3(0.0, 0.0, 0.0);"); src.push("vec3 viewLightDir = vec3(0.0, 0.0, -1.0);"); src.push("float lambertian = 1.0;"); src.push("vec3 xTangent = dFdx( vViewPosition.xyz );"); src.push("vec3 yTangent = dFdy( vViewPosition.xyz );"); src.push("vec3 viewNormal = normalize( cross( xTangent, yTangent ) );"); for (let i = 0, len = lightsState.lights.length; i < len; i++) { const light = lightsState.lights[i]; if (light.type === "ambient") { continue; } if (light.type === "dir") { if (light.space === "view") { src.push("viewLightDir = normalize(lightDir" + i + ");"); } else { src.push("viewLightDir = normalize((viewMatrix * vec4(lightDir" + i + ", 0.0)).xyz);"); } } else if (light.type === "point") { if (light.space === "view") { src.push("viewLightDir = -normalize(lightPos" + i + " - viewPosition.xyz);"); } else { src.push("viewLightDir = -normalize((viewMatrix * vec4(lightPos" + i + ", 0.0)).xyz);"); } } else if (light.type === "spot") { if (light.space === "view") { src.push("viewLightDir = normalize(lightDir" + i + ");"); } else { src.push("viewLightDir = normalize((viewMatrix * vec4(lightDir" + i + ", 0.0)).xyz);"); } } else { continue; } src.push("lambertian = max(dot(-viewNormal, viewLightDir), 0.0);"); src.push("reflectedColor += lambertian * (lightColor" + i + ".rgb * lightColor" + i + ".a);"); } src.push("vec4 color = vec4((lightAmbient.rgb * lightAmbient.a * newColor.rgb) + (reflectedColor * newColor.rgb), newColor.a);"); src.push("vec4 sampleColor = texture(uColorMap, vUV);"); if (useAlphaCutoff) { src.push("if (sampleColor.a < materialAlphaCutoff) {"); src.push(" discard;"); src.push("}"); } if (gammaOutput) { src.push("sampleColor = sRGBToLinear(sampleColor);"); } src.push("vec4 colorTexel = color * sampleColor;"); src.push("float opacity = color.a;"); if (this._withSAO) { // Doing SAO blend in the main solid fill draw shader just so that edge lines can be drawn over the top // Would be more efficient to defer this, then render lines later, using same depth buffer for Z-reject src.push(" float viewportWidth = uSAOParams[0];"); src.push(" float viewportHeight = uSAOParams[1];"); src.push(" float blendCutoff = uSAOParams[2];"); src.push(" float blendFactor = uSAOParams[3];"); src.push(" vec2 uv = vec2(gl_FragCoord.x / viewportWidth, gl_FragCoord.y / viewportHeight);"); src.push(" float ambient = smoothstep(blendCutoff, 1.0, unpackRGBToFloat(texture(uOcclusionTexture, uv))) * blendFactor;"); src.push(" outColor = vec4(colorTexel.rgb * ambient, opacity);"); } else { src.push(" outColor = vec4(colorTexel.rgb, opacity);"); } if (gammaOutput) { src.push("outColor = linearToGamma(outColor, gammaFactor);"); } if (scene.logarithmicDepthBufferEnabled) { src.push("gl_FragDepth = isPerspective == 0.0 ? gl_FragCoord.z : log2( vFragDepth ) * logDepthBufFC * 0.5;"); } src.push("}"); return src; } } const tempVec3a$E = math.vec3(); const tempVec3b$A = math.vec3(); const tempVec3c$v = math.vec3(); const tempVec3d$e = math.vec3(); const tempMat4a$q = math.mat4(); /** * @private */ class TrianglesSnapInitRenderer$1 extends VBORenderer { drawLayer(frameCtx, batchingLayer, renderPass) { if (!this._program) { this._allocate(); if (this.errors) { return; } } if (frameCtx.lastProgramId !== this._program.id) { frameCtx.lastProgramId = this._program.id; this._bindProgram(); } const model = batchingLayer.model; const scene = model.scene; const camera = scene.camera; const gl = scene.canvas.gl; const state = batchingLayer._state; const origin = batchingLayer._state.origin; const {position, rotationMatrix} = model; const aabb = batchingLayer.aabb; // Per-layer AABB for best RTC accuracy const viewMatrix = frameCtx.pickViewMatrix || camera.viewMatrix; if (this._vaoCache.has(batchingLayer)) { gl.bindVertexArray(this._vaoCache.get(batchingLayer)); } else { this._vaoCache.set(batchingLayer, this._makeVAO(state)); } const coordinateScaler = tempVec3a$E; coordinateScaler[0] = math.safeInv(aabb[3] - aabb[0]) * math.MAX_INT; coordinateScaler[1] = math.safeInv(aabb[4] - aabb[1]) * math.MAX_INT; coordinateScaler[2] = math.safeInv(aabb[5] - aabb[2]) * math.MAX_INT; frameCtx.snapPickCoordinateScale[0] = math.safeInv(coordinateScaler[0]); frameCtx.snapPickCoordinateScale[1] = math.safeInv(coordinateScaler[1]); frameCtx.snapPickCoordinateScale[2] = math.safeInv(coordinateScaler[2]); let rtcViewMatrix; let rtcCameraEye; if (origin || position[0] !== 0 || position[1] !== 0 || position[2] !== 0) { const rtcOrigin = tempVec3b$A; if (origin) { const rotatedOrigin = tempVec3c$v; math.transformPoint3(rotationMatrix, origin, rotatedOrigin); rtcOrigin[0] = rotatedOrigin[0]; rtcOrigin[1] = rotatedOrigin[1]; rtcOrigin[2] = rotatedOrigin[2]; } else { rtcOrigin[0] = 0; rtcOrigin[1] = 0; rtcOrigin[2] = 0; } rtcOrigin[0] += position[0]; rtcOrigin[1] += position[1]; rtcOrigin[2] += position[2]; rtcViewMatrix = createRTCViewMat(viewMatrix, rtcOrigin, tempMat4a$q); rtcCameraEye = tempVec3d$e; rtcCameraEye[0] = camera.eye[0] - rtcOrigin[0]; rtcCameraEye[1] = camera.eye[1] - rtcOrigin[1]; rtcCameraEye[2] = camera.eye[2] - rtcOrigin[2]; frameCtx.snapPickOrigin[0] = rtcOrigin[0]; frameCtx.snapPickOrigin[1] = rtcOrigin[1]; frameCtx.snapPickOrigin[2] = rtcOrigin[2]; } else { rtcViewMatrix = viewMatrix; rtcCameraEye = camera.eye; frameCtx.snapPickOrigin[0] = 0; frameCtx.snapPickOrigin[1] = 0; frameCtx.snapPickOrigin[2] = 0; } gl.uniform3fv(this._uCameraEyeRtc, rtcCameraEye); gl.uniform2fv(this.uVectorA, frameCtx.snapVectorA); gl.uniform2fv(this.uInverseVectorAB, frameCtx.snapInvVectorAB); gl.uniform1i(this._uLayerNumber, frameCtx.snapPickLayerNumber); gl.uniform3fv(this._uCoordinateScaler, coordinateScaler); gl.uniform1i(this._uRenderPass, renderPass); gl.uniform1i(this._uPickInvisible, frameCtx.pickInvisible); let offset = 0; const mat4Size = 4 * 4; this._matricesUniformBlockBufferData.set(rotationMatrix, 0); this._matricesUniformBlockBufferData.set(rtcViewMatrix, offset += mat4Size); this._matricesUniformBlockBufferData.set(camera.projMatrix, offset += mat4Size); this._matricesUniformBlockBufferData.set(state.positionsDecodeMatrix, offset += mat4Size); gl.bindBuffer(gl.UNIFORM_BUFFER, this._matricesUniformBlockBuffer); gl.bufferData(gl.UNIFORM_BUFFER, this._matricesUniformBlockBufferData, gl.DYNAMIC_DRAW); gl.bindBufferBase( gl.UNIFORM_BUFFER, this._matricesUniformBlockBufferBindingPoint, this._matricesUniformBlockBuffer); { const logDepthBufFC = 2.0 / (Math.log(frameCtx.pickZFar + 1.0) / Math.LN2); // TODO: Far from pick project matrix? gl.uniform1f(this._uLogDepthBufFC, logDepthBufFC); } this.setSectionPlanesStateUniforms(batchingLayer); //============================================================= // TODO: Use drawElements count and offset to draw only one entity //============================================================= state.indicesBuf.bind(); gl.drawElements(gl.TRIANGLES, state.indicesBuf.numItems, state.indicesBuf.itemType, 0); state.indicesBuf.unbind(); gl.bindVertexArray(null); } _allocate() { super._allocate(); const program = this._program; { this._uLogDepthBufFC = program.getLocation("logDepthBufFC"); } this._uCameraEyeRtc = program.getLocation("uCameraEyeRtc"); this.uVectorA = program.getLocation("snapVectorA"); this.uInverseVectorAB = program.getLocation("snapInvVectorAB"); this._uLayerNumber = program.getLocation("layerNumber"); this._uCoordinateScaler = program.getLocation("coordinateScaler"); } _bindProgram() { this._program.bind(); } _buildVertexShader() { const scene = this._scene; const clipping = scene._sectionPlanesState.getNumAllocatedSectionPlanes() > 0; const src = []; src.push ('#version 300 es'); src.push("// VBO SnapBatchingDepthBufInitRenderer vertex shader"); src.push("#ifdef GL_FRAGMENT_PRECISION_HIGH"); src.push("precision highp float;"); src.push("precision highp int;"); src.push("precision highp usampler2D;"); src.push("precision highp isampler2D;"); src.push("precision highp sampler2D;"); src.push("#else"); src.push("precision mediump float;"); src.push("precision mediump int;"); src.push("precision mediump usampler2D;"); src.push("precision mediump isampler2D;"); src.push("precision mediump sampler2D;"); src.push("#endif"); src.push("uniform int renderPass;"); src.push("in vec4 pickColor;"); src.push("in vec3 position;"); if (scene.entityOffsetsEnabled) { src.push("in vec3 offset;"); } src.push("in float flags;"); src.push("uniform bool pickInvisible;"); this._addMatricesUniformBlockLines(src); src.push("uniform vec3 uCameraEyeRtc;"); src.push("uniform vec2 snapVectorA;"); src.push("uniform vec2 snapInvVectorAB;"); { src.push("uniform float logDepthBufFC;"); src.push("out float vFragDepth;"); src.push("out float isPerspective;"); } src.push("bool isPerspectiveMatrix(mat4 m) {"); src.push(" return (m[2][3] == - 1.0);"); src.push("}"); src.push("vec2 remapClipPos(vec2 clipPos) {"); src.push(" float x = (clipPos.x - snapVectorA.x) * snapInvVectorAB.x;"); src.push(" float y = (clipPos.y - snapVectorA.y) * snapInvVectorAB.y;"); src.push(" return vec2(x, y);"); src.push("}"); src.push("flat out vec4 vPickColor;"); src.push("out vec4 vWorldPosition;"); if (clipping) { src.push("out float vFlags;"); } src.push("out highp vec3 relativeToOriginPosition;"); src.push("void main(void) {"); // pickFlag = NOT_RENDERED | PICK // renderPass = PICK src.push(`int pickFlag = int(flags) >> 12 & 0xF;`); src.push(`if (pickFlag != renderPass) {`); src.push(" gl_Position = vec4(0.0, 0.0, 0.0, 0.0);"); // Cull vertex src.push(" } else {"); src.push(" vec4 worldPosition = worldMatrix * (positionsDecodeMatrix * vec4(position, 1.0)); "); if (scene.entityOffsetsEnabled) { src.push(" worldPosition.xyz = worldPosition.xyz + offset;"); } src.push(" relativeToOriginPosition = worldPosition.xyz;"); src.push(" vec4 viewPosition = viewMatrix * worldPosition; "); src.push(" vWorldPosition = worldPosition;"); if (clipping) { src.push(" vFlags = flags;"); } src.push("vPickColor = pickColor;"); src.push("vec4 clipPos = projMatrix * viewPosition;"); src.push("float tmp = clipPos.w;"); src.push("clipPos.xyzw /= tmp;"); src.push("clipPos.xy = remapClipPos(clipPos.xy);"); src.push("clipPos.xyzw *= tmp;"); { src.push("vFragDepth = 1.0 + clipPos.w;"); src.push("isPerspective = float (isPerspectiveMatrix(projMatrix));"); } src.push("gl_Position = clipPos;"); src.push(" }"); src.push("}"); return src; } _buildFragmentShader() { const scene = this._scene; const sectionPlanesState = scene._sectionPlanesState; const clipping = sectionPlanesState.getNumAllocatedSectionPlanes() > 0; const src = []; src.push ('#version 300 es'); src.push("// VBO SnapBatchingDepthBufInitRenderer fragment shader"); src.push("#ifdef GL_FRAGMENT_PRECISION_HIGH"); src.push("precision highp float;"); src.push("precision highp int;"); src.push("#else"); src.push("precision mediump float;"); src.push("precision mediump int;"); src.push("#endif"); { src.push("in float isPerspective;"); src.push("uniform float logDepthBufFC;"); src.push("in float vFragDepth;"); } src.push("uniform int layerNumber;"); src.push("uniform vec3 coordinateScaler;"); src.push("in vec4 vWorldPosition;"); src.push("flat in vec4 vPickColor;"); if (clipping) { src.push("in float vFlags;"); for (let i = 0; i < sectionPlanesState.getNumAllocatedSectionPlanes(); i++) { src.push("uniform bool sectionPlaneActive" + i + ";"); src.push("uniform vec3 sectionPlanePos" + i + ";"); src.push("uniform vec3 sectionPlaneDir" + i + ";"); } } src.push("in highp vec3 relativeToOriginPosition;"); src.push("layout(location = 0) out highp ivec4 outCoords;"); src.push("layout(location = 1) out highp ivec4 outNormal;"); src.push("layout(location = 2) out lowp uvec4 outPickColor;"); src.push("void main(void) {"); if (clipping) { src.push(" bool clippable = (int(vFlags) >> 16 & 0xF) == 1;"); src.push(" if (clippable) {"); src.push(" float dist = 0.0;"); for (var i = 0; i < sectionPlanesState.getNumAllocatedSectionPlanes(); i++) { src.push(" if (sectionPlaneActive" + i + ") {"); src.push(" dist += clamp(dot(-sectionPlaneDir" + i + ".xyz, vWorldPosition.xyz - sectionPlanePos" + i + ".xyz), 0.0, 1000.0);"); src.push(" }"); } src.push(" if (dist > 0.0) { discard; }"); src.push(" }"); } { src.push(" float dx = dFdx(vFragDepth);"); src.push(" float dy = dFdy(vFragDepth);"); src.push(" float diff = sqrt(dx*dx+dy*dy);"); src.push(" gl_FragDepth = isPerspective == 0.0 ? gl_FragCoord.z : log2( vFragDepth + diff ) * logDepthBufFC * 0.5;"); } src.push("outCoords = ivec4(relativeToOriginPosition.xyz*coordinateScaler.xyz, -layerNumber);"); src.push("vec3 xTangent = dFdx( vWorldPosition.xyz );"); src.push("vec3 yTangent = dFdy( vWorldPosition.xyz );"); src.push("vec3 worldNormal = normalize( cross( xTangent, yTangent ) );"); src.push(`outNormal = ivec4(worldNormal * float(${math.MAX_INT}), 1.0);`); src.push("outPickColor = uvec4(vPickColor);"); src.push("}"); return src; } webglContextRestored() { this._program = null; } destroy() { if (this._program) { this._program.destroy(); } this._program = null; } } const tempVec3a$D = math.vec3(); const tempVec3b$z = math.vec3(); const tempVec3c$u = math.vec3(); const tempVec3d$d = math.vec3(); const tempMat4a$p = math.mat4(); /** * @private */ class TrianglesSnapRenderer$1 extends VBORenderer{ _getHash() { return this._scene._sectionPlanesState.getHash() + (this._scene.pointsMaterial.hash); } drawLayer(frameCtx, batchingLayer, renderPass) { if (!this._program) { this._allocate(); if (this.errors) { return; } } if (frameCtx.lastProgramId !== this._program.id) { frameCtx.lastProgramId = this._program.id; this._bindProgram(); } const model = batchingLayer.model; const scene = model.scene; const camera = scene.camera; const gl = scene.canvas.gl; const state = batchingLayer._state; const origin = batchingLayer._state.origin; const {position, rotationMatrix} = model; const aabb = batchingLayer.aabb; // Per-layer AABB for best RTC accuracy const viewMatrix = frameCtx.pickViewMatrix || camera.viewMatrix; if (this._vaoCache.has(batchingLayer)) { gl.bindVertexArray(this._vaoCache.get(batchingLayer)); } else { this._vaoCache.set(batchingLayer, this._makeVAO(state)); } const coordinateScaler = tempVec3a$D; coordinateScaler[0] = math.safeInv(aabb[3] - aabb[0]) * math.MAX_INT; coordinateScaler[1] = math.safeInv(aabb[4] - aabb[1]) * math.MAX_INT; coordinateScaler[2] = math.safeInv(aabb[5] - aabb[2]) * math.MAX_INT; frameCtx.snapPickCoordinateScale[0] = math.safeInv(coordinateScaler[0]); frameCtx.snapPickCoordinateScale[1] = math.safeInv(coordinateScaler[1]); frameCtx.snapPickCoordinateScale[2] = math.safeInv(coordinateScaler[2]); let rtcViewMatrix; let rtcCameraEye; if (origin || position[0] !== 0 || position[1] !== 0 || position[2] !== 0) { const rtcOrigin = tempVec3b$z; if (origin) { const rotatedOrigin = tempVec3c$u; math.transformPoint3(rotationMatrix, origin, rotatedOrigin); rtcOrigin[0] = rotatedOrigin[0]; rtcOrigin[1] = rotatedOrigin[1]; rtcOrigin[2] = rotatedOrigin[2]; } else { rtcOrigin[0] = 0; rtcOrigin[1] = 0; rtcOrigin[2] = 0; } rtcOrigin[0] += position[0]; rtcOrigin[1] += position[1]; rtcOrigin[2] += position[2]; rtcViewMatrix = createRTCViewMat(viewMatrix, rtcOrigin, tempMat4a$p); rtcCameraEye = tempVec3d$d; rtcCameraEye[0] = camera.eye[0] - rtcOrigin[0]; rtcCameraEye[1] = camera.eye[1] - rtcOrigin[1]; rtcCameraEye[2] = camera.eye[2] - rtcOrigin[2]; frameCtx.snapPickOrigin[0] = rtcOrigin[0]; frameCtx.snapPickOrigin[1] = rtcOrigin[1]; frameCtx.snapPickOrigin[2] = rtcOrigin[2]; } else { rtcViewMatrix = viewMatrix; rtcCameraEye = camera.eye; frameCtx.snapPickOrigin[0] = 0; frameCtx.snapPickOrigin[1] = 0; frameCtx.snapPickOrigin[2] = 0; } gl.uniform3fv(this._uCameraEyeRtc, rtcCameraEye); gl.uniform2fv(this.uVectorA, frameCtx.snapVectorA); gl.uniform2fv(this.uInverseVectorAB, frameCtx.snapInvVectorAB); gl.uniform1i(this._uLayerNumber, frameCtx.snapPickLayerNumber); gl.uniform3fv(this._uCoordinateScaler, coordinateScaler); gl.uniform1i(this._uRenderPass, renderPass); gl.uniform1i(this._uPickInvisible, frameCtx.pickInvisible); let offset = 0; const mat4Size = 4 * 4; this._matricesUniformBlockBufferData.set(rotationMatrix, 0); this._matricesUniformBlockBufferData.set(rtcViewMatrix, offset += mat4Size); this._matricesUniformBlockBufferData.set(camera.projMatrix, offset += mat4Size); this._matricesUniformBlockBufferData.set(state.positionsDecodeMatrix, offset += mat4Size); gl.bindBuffer(gl.UNIFORM_BUFFER, this._matricesUniformBlockBuffer); gl.bufferData(gl.UNIFORM_BUFFER, this._matricesUniformBlockBufferData, gl.DYNAMIC_DRAW); gl.bindBufferBase( gl.UNIFORM_BUFFER, this._matricesUniformBlockBufferBindingPoint, this._matricesUniformBlockBuffer); { const logDepthBufFC = 2.0 / (Math.log(frameCtx.pickZFar + 1.0) / Math.LN2); // TODO: Far from pick project matrix? gl.uniform1f(this._uLogDepthBufFC, logDepthBufFC); } this.setSectionPlanesStateUniforms(batchingLayer); //============================================================= // TODO: Use drawElements count and offset to draw only one entity //============================================================= if (frameCtx.snapMode === "edge") { if (state.edgeIndicesBuf) { state.edgeIndicesBuf.bind(); gl.drawElements(gl.LINES, state.edgeIndicesBuf.numItems, state.edgeIndicesBuf.itemType, 0); state.edgeIndicesBuf.unbind(); // needed? } } else { gl.drawArrays(gl.POINTS, 0, state.positionsBuf.numItems); } gl.bindVertexArray(null); } _allocate() { super._allocate(); const program = this._program; { this._uLogDepthBufFC = program.getLocation("logDepthBufFC"); } this._uCameraEyeRtc = program.getLocation("uCameraEyeRtc"); this.uVectorA = program.getLocation("snapVectorA"); this.uInverseVectorAB = program.getLocation("snapInvVectorAB"); this._uLayerNumber = program.getLocation("layerNumber"); this._uCoordinateScaler = program.getLocation("coordinateScaler"); } _bindProgram() { this._program.bind(); } _buildVertexShader() { const scene = this._scene; const clipping = scene._sectionPlanesState.getNumAllocatedSectionPlanes() > 0; scene.pointsMaterial._state; const src = []; src.push ('#version 300 es'); src.push("// SnapBatchingDepthRenderer vertex shader"); src.push("#ifdef GL_FRAGMENT_PRECISION_HIGH"); src.push("precision highp float;"); src.push("precision highp int;"); src.push("precision highp usampler2D;"); src.push("precision highp isampler2D;"); src.push("precision highp sampler2D;"); src.push("#else"); src.push("precision mediump float;"); src.push("precision mediump int;"); src.push("precision mediump usampler2D;"); src.push("precision mediump isampler2D;"); src.push("precision mediump sampler2D;"); src.push("#endif"); src.push("uniform int renderPass;"); src.push("in vec3 position;"); if (scene.entityOffsetsEnabled) { src.push("in vec3 offset;"); } src.push("in float flags;"); src.push("uniform bool pickInvisible;"); this._addMatricesUniformBlockLines(src); src.push("uniform vec3 uCameraEyeRtc;"); src.push("uniform vec2 snapVectorA;"); src.push("uniform vec2 snapInvVectorAB;"); { src.push("uniform float logDepthBufFC;"); src.push("out float vFragDepth;"); src.push("bool isPerspectiveMatrix(mat4 m) {"); src.push(" return (m[2][3] == - 1.0);"); src.push("}"); src.push("out float isPerspective;"); } src.push("vec2 remapClipPos(vec2 clipPos) {"); src.push(" float x = (clipPos.x - snapVectorA.x) * snapInvVectorAB.x;"); src.push(" float y = (clipPos.y - snapVectorA.y) * snapInvVectorAB.y;"); src.push(" return vec2(x, y);"); src.push("}"); if (clipping) { src.push("out vec4 vWorldPosition;"); src.push("out float vFlags;"); } src.push("out highp vec3 relativeToOriginPosition;"); src.push("void main(void) {"); // pickFlag = NOT_RENDERED | PICK // renderPass = PICK src.push(`int pickFlag = int(flags) >> 12 & 0xF;`); src.push(`if (pickFlag != renderPass) {`); src.push(" gl_Position = vec4(2.0, 0.0, 0.0, 0.0);"); // Cull vertex src.push(" } else {"); src.push(" vec4 worldPosition = worldMatrix * (positionsDecodeMatrix * vec4(position, 1.0)); "); if (scene.entityOffsetsEnabled) { src.push(" worldPosition.xyz = worldPosition.xyz + offset;"); } src.push("relativeToOriginPosition = worldPosition.xyz;"); src.push(" vec4 viewPosition = viewMatrix * worldPosition; "); if (clipping) { src.push(" vWorldPosition = worldPosition;"); src.push(" vFlags = flags;"); } src.push("vec4 clipPos = projMatrix * viewPosition;"); src.push("float tmp = clipPos.w;"); src.push("clipPos.xyzw /= tmp;"); src.push("clipPos.xy = remapClipPos(clipPos.xy);"); src.push("clipPos.xyzw *= tmp;"); { src.push("vFragDepth = 1.0 + clipPos.w;"); src.push("isPerspective = float (isPerspectiveMatrix(projMatrix));"); } src.push("gl_Position = clipPos;"); src.push("gl_PointSize = 1.0;"); // Windows needs this? src.push(" }"); src.push("}"); return src; } _buildFragmentShader() { const scene = this._scene; const sectionPlanesState = scene._sectionPlanesState; const clipping = sectionPlanesState.getNumAllocatedSectionPlanes() > 0; const src = []; src.push ('#version 300 es'); src.push("// SnapBatchingDepthRenderer fragment shader"); src.push("#ifdef GL_FRAGMENT_PRECISION_HIGH"); src.push("precision highp float;"); src.push("precision highp int;"); src.push("#else"); src.push("precision mediump float;"); src.push("precision mediump int;"); src.push("#endif"); { src.push("in float isPerspective;"); src.push("uniform float logDepthBufFC;"); src.push("in float vFragDepth;"); } src.push("uniform int layerNumber;"); src.push("uniform vec3 coordinateScaler;"); if (clipping) { src.push("in vec4 vWorldPosition;"); src.push("in float vFlags;"); for (let i = 0; i < sectionPlanesState.getNumAllocatedSectionPlanes(); i++) { src.push("uniform bool sectionPlaneActive" + i + ";"); src.push("uniform vec3 sectionPlanePos" + i + ";"); src.push("uniform vec3 sectionPlaneDir" + i + ";"); } } src.push("in highp vec3 relativeToOriginPosition;"); src.push("out highp ivec4 outCoords;"); src.push("void main(void) {"); if (clipping) { src.push(" bool clippable = (int(vFlags) >> 16 & 0xF) == 1;"); src.push(" if (clippable) {"); src.push(" float dist = 0.0;"); for (var i = 0; i < sectionPlanesState.getNumAllocatedSectionPlanes(); i++) { src.push(" if (sectionPlaneActive" + i + ") {"); src.push(" dist += clamp(dot(-sectionPlaneDir" + i + ".xyz, vWorldPosition.xyz - sectionPlanePos" + i + ".xyz), 0.0, 1000.0);"); src.push(" }"); } src.push(" if (dist > 0.0) { discard; }"); src.push(" }"); } { src.push(" gl_FragDepth = isPerspective == 0.0 ? gl_FragCoord.z : log2( vFragDepth ) * logDepthBufFC * 0.5;"); } src.push("outCoords = ivec4(relativeToOriginPosition.xyz*coordinateScaler.xyz, layerNumber);"); src.push("}"); return src; } webglContextRestored() { this._program = null; } destroy() { if (this._program) { this._program.destroy(); } this._program = null; } } /** * @private */ class Renderers$1 { constructor(scene) { this._scene = scene; } _compile() { if (this._colorRenderer && (!this._colorRenderer.getValid())) { this._colorRenderer.destroy(); this._colorRenderer = null; } if (this._colorRendererWithSAO && (!this._colorRendererWithSAO.getValid())) { this._colorRendererWithSAO.destroy(); this._colorRendererWithSAO = null; } if (this._flatColorRenderer && (!this._flatColorRenderer.getValid())) { this._flatColorRenderer.destroy(); this._flatColorRenderer = null; } if (this._flatColorRendererWithSAO && (!this._flatColorRendererWithSAO.getValid())) { this._flatColorRendererWithSAO.destroy(); this._flatColorRendererWithSAO = null; } if (this._colorTextureRenderer && (!this._colorTextureRenderer.getValid())) { this._colorTextureRenderer.destroy(); this._colorTextureRenderer = null; } if (this._colorTextureRendererWithSAO && (!this._colorTextureRendererWithSAO.getValid())) { this._colorTextureRendererWithSAO.destroy(); this._colorTextureRendererWithSAO = null; } if (this._colorTextureRendererAlphaCutoff && (!this._colorTextureRendererAlphaCutoff.getValid())) { this._colorTextureRendererAlphaCutoff.destroy(); this._colorTextureRendererAlphaCutoff = null; } if (this._colorTextureRendererWithSAOAlphaCutoff && (!this._colorTextureRendererWithSAOAlphaCutoff.getValid())) { this._colorTextureRendererWithSAOAlphaCutoff.destroy(); this._colorTextureRendererWithSAOAlphaCutoff = null; } if (this._pbrRenderer && (!this._pbrRenderer.getValid())) { this._pbrRenderer.destroy(); this._pbrRenderer = null; } if (this._pbrRendererWithSAO && (!this._pbrRendererWithSAO.getValid())) { this._pbrRendererWithSAO.destroy(); this._pbrRendererWithSAO = null; } if (this._depthRenderer && (!this._depthRenderer.getValid())) { this._depthRenderer.destroy(); this._depthRenderer = null; } if (this._normalsRenderer && (!this._normalsRenderer.getValid())) { this._normalsRenderer.destroy(); this._normalsRenderer = null; } if (this._silhouetteRenderer && (!this._silhouetteRenderer.getValid())) { this._silhouetteRenderer.destroy(); this._silhouetteRenderer = null; } if (this._edgesRenderer && (!this._edgesRenderer.getValid())) { this._edgesRenderer.destroy(); this._edgesRenderer = null; } if (this._edgesColorRenderer && (!this._edgesColorRenderer.getValid())) { this._edgesColorRenderer.destroy(); this._edgesColorRenderer = null; } if (this._pickMeshRenderer && (!this._pickMeshRenderer.getValid())) { this._pickMeshRenderer.destroy(); this._pickMeshRenderer = null; } if (this._pickDepthRenderer && (!this._pickDepthRenderer.getValid())) { this._pickDepthRenderer.destroy(); this._pickDepthRenderer = null; } if (this._pickNormalsRenderer && this._pickNormalsRenderer.getValid() === false) { this._pickNormalsRenderer.destroy(); this._pickNormalsRenderer = null; } if (this._pickNormalsFlatRenderer && this._pickNormalsFlatRenderer.getValid() === false) { this._pickNormalsFlatRenderer.destroy(); this._pickNormalsFlatRenderer = null; } if (this._occlusionRenderer && this._occlusionRenderer.getValid() === false) { this._occlusionRenderer.destroy(); this._occlusionRenderer = null; } if (this._shadowRenderer && (!this._shadowRenderer.getValid())) { this._shadowRenderer.destroy(); this._shadowRenderer = null; } if (this._snapInitRenderer && (!this._snapInitRenderer.getValid())) { this._snapInitRenderer.destroy(); this._snapInitRenderer = null; } if (this._snapRenderer && (!this._snapRenderer.getValid())) { this._snapRenderer.destroy(); this._snapRenderer = null; } } eagerCreateRenders() { // Pre-initialize certain renderers that would otherwise be lazy-initialised // on user interaction, such as picking or emphasis, so that there is no delay // when user first begins interacting with the viewer. if (!this._silhouetteRenderer) { // Used for highlighting and selection this._silhouetteRenderer = new TrianglesSilhouetteRenderer$1(this._scene); } if (!this._pickMeshRenderer) { this._pickMeshRenderer = new TrianglesPickMeshRenderer$1(this._scene); } if (!this._pickDepthRenderer) { this._pickDepthRenderer = new TrianglesPickDepthRenderer$1(this._scene); } if (!this._snapInitRenderer) { this._snapInitRenderer = new TrianglesSnapInitRenderer$1(this._scene, false); } if (!this._snapRenderer) { this._snapRenderer = new TrianglesSnapRenderer$1(this._scene); } } get colorRenderer() { if (!this._colorRenderer) { this._colorRenderer = new TrianglesColorRenderer$1(this._scene, false); } return this._colorRenderer; } get colorRendererWithSAO() { if (!this._colorRendererWithSAO) { this._colorRendererWithSAO = new TrianglesColorRenderer$1(this._scene, true); } return this._colorRendererWithSAO; } get flatColorRenderer() { if (!this._flatColorRenderer) { this._flatColorRenderer = new TrianglesFlatColorRenderer$1(this._scene, false); } return this._flatColorRenderer; } get flatColorRendererWithSAO() { if (!this._flatColorRendererWithSAO) { this._flatColorRendererWithSAO = new TrianglesFlatColorRenderer$1(this._scene, true); } return this._flatColorRendererWithSAO; } get colorTextureRenderer() { if (!this._colorTextureRenderer) { this._colorTextureRenderer = new TrianglesColorTextureRenderer$1(this._scene, false); } return this._colorTextureRenderer; } get colorTextureRendererWithSAO() { if (!this._colorTextureRendererWithSAO) { this._colorTextureRendererWithSAO = new TrianglesColorTextureRenderer$1(this._scene, true); } return this._colorTextureRendererWithSAO; } get colorTextureRendererAlphaCutoff() { if (!this._colorTextureRendererAlphaCutoff) { this._colorTextureRendererAlphaCutoff = new TrianglesColorTextureRenderer$1(this._scene, false, { useAlphaCutoff: true }); } return this._colorTextureRendererAlphaCutoff; } get colorTextureRendererWithSAOAlphaCutoff() { if (!this._colorTextureRendererWithSAOAlphaCutoff) { this._colorTextureRendererWithSAOAlphaCutoff = new TrianglesColorTextureRenderer$1(this._scene, true, { useAlphaCutoff: true }); } return this._colorTextureRendererWithSAOAlphaCutoff; } get pbrRenderer() { if (!this._pbrRenderer) { this._pbrRenderer = new TrianglesPBRRenderer$1(this._scene, false); } return this._pbrRenderer; } get pbrRendererWithSAO() { if (!this._pbrRendererWithSAO) { this._pbrRendererWithSAO = new TrianglesPBRRenderer$1(this._scene, true); } return this._pbrRendererWithSAO; } get silhouetteRenderer() { if (!this._silhouetteRenderer) { this._silhouetteRenderer = new TrianglesSilhouetteRenderer$1(this._scene); } return this._silhouetteRenderer; } get depthRenderer() { if (!this._depthRenderer) { this._depthRenderer = new TrianglesDepthRenderer$1(this._scene); } return this._depthRenderer; } get normalsRenderer() { if (!this._normalsRenderer) { this._normalsRenderer = new TrianglesNormalsRenderer$1(this._scene); } return this._normalsRenderer; } get edgesRenderer() { if (!this._edgesRenderer) { this._edgesRenderer = new EdgesEmphasisRenderer$1(this._scene); } return this._edgesRenderer; } get edgesColorRenderer() { if (!this._edgesColorRenderer) { this._edgesColorRenderer = new EdgesColorRenderer$1(this._scene); } return this._edgesColorRenderer; } get pickMeshRenderer() { if (!this._pickMeshRenderer) { this._pickMeshRenderer = new TrianglesPickMeshRenderer$1(this._scene); } return this._pickMeshRenderer; } get pickNormalsRenderer() { if (!this._pickNormalsRenderer) { this._pickNormalsRenderer = new TrianglesPickNormalsRenderer$1(this._scene); } return this._pickNormalsRenderer; } get pickNormalsFlatRenderer() { if (!this._pickNormalsFlatRenderer) { this._pickNormalsFlatRenderer = new TrianglesPickNormalsFlatRenderer$1(this._scene); } return this._pickNormalsFlatRenderer; } get pickDepthRenderer() { if (!this._pickDepthRenderer) { this._pickDepthRenderer = new TrianglesPickDepthRenderer$1(this._scene); } return this._pickDepthRenderer; } get occlusionRenderer() { if (!this._occlusionRenderer) { this._occlusionRenderer = new TrianglesOcclusionRenderer$1(this._scene); } return this._occlusionRenderer; } get shadowRenderer() { if (!this._shadowRenderer) { this._shadowRenderer = new TrianglesShadowRenderer$1(this._scene); } return this._shadowRenderer; } get snapRenderer() { if (!this._snapRenderer) { this._snapRenderer = new TrianglesSnapRenderer$1(this._scene); } return this._snapRenderer; } get snapInitRenderer() { if (!this._snapInitRenderer) { this._snapInitRenderer = new TrianglesSnapInitRenderer$1(this._scene); } return this._snapInitRenderer; } _destroy() { if (this._colorRenderer) { this._colorRenderer.destroy(); } if (this._colorRendererWithSAO) { this._colorRendererWithSAO.destroy(); } if (this._flatColorRenderer) { this._flatColorRenderer.destroy(); } if (this._flatColorRendererWithSAO) { this._flatColorRendererWithSAO.destroy(); } if (this._colorTextureRenderer) { this._colorTextureRenderer.destroy(); } if (this._colorTextureRendererWithSAO) { this._colorTextureRendererWithSAO.destroy(); } if (this._colorTextureRendererAlphaCutoff) { this._colorTextureRendererAlphaCutoff.destroy(); } if (this._colorTextureRendererWithSAOAlphaCutoff) { this._colorTextureRendererWithSAOAlphaCutoff.destroy(); } if (this._pbrRenderer) { this._pbrRenderer.destroy(); } if (this._pbrRendererWithSAO) { this._pbrRendererWithSAO.destroy(); } if (this._depthRenderer) { this._depthRenderer.destroy(); } if (this._normalsRenderer) { this._normalsRenderer.destroy(); } if (this._silhouetteRenderer) { this._silhouetteRenderer.destroy(); } if (this._edgesRenderer) { this._edgesRenderer.destroy(); } if (this._edgesColorRenderer) { this._edgesColorRenderer.destroy(); } if (this._pickMeshRenderer) { this._pickMeshRenderer.destroy(); } if (this._pickDepthRenderer) { this._pickDepthRenderer.destroy(); } if (this._pickNormalsRenderer) { this._pickNormalsRenderer.destroy(); } if (this._pickNormalsFlatRenderer) { this._pickNormalsFlatRenderer.destroy(); } if (this._occlusionRenderer) { this._occlusionRenderer.destroy(); } if (this._shadowRenderer) { this._shadowRenderer.destroy(); } if (this._snapInitRenderer) { this._snapInitRenderer.destroy(); } if (this._snapRenderer) { this._snapRenderer.destroy(); } } } const cachedRenderers$6 = {}; /** * @private */ function getRenderers$7(scene) { const sceneId = scene.id; let renderers = cachedRenderers$6[sceneId]; if (!renderers) { renderers = new Renderers$1(scene); cachedRenderers$6[sceneId] = renderers; renderers._compile(); renderers.eagerCreateRenders(); scene.on("compile", () => { renderers._compile(); renderers.eagerCreateRenders(); }); scene.on("destroyed", () => { delete cachedRenderers$6[sceneId]; renderers._destroy(); }); } return renderers; } let maxDataTextureHeight = 1 << 16; let maxGeometryBatchSize = 5000000; /** * Manages global configurations for all {@link Viewer}s. * * ## Example * * In the example below, we'll disable xeokit's double-precision support, which gives a performance and memory boost * on low-power devices, but also means that we can no longer render double-precision models without jittering. * * That's OK if we know that we're not going to view models that are geographically vast, or offset far from the World coordinate origin. * * [[Run this example](/examples/index.html#Configs_disableDoublePrecisionAndRAF)] * * ````javascript * import {Configs, Viewer, XKTLoaderPlugin} from "https://cdn.jsdelivr.net/npm/@xeokit/xeokit-sdk/dist/xeokit-sdk.es.min.js"; * * // Access xeoit-sdk global configs. * // We typically set configs only before we create any Viewers. * const configs = new Configs(); * * // Disable 64-bit precision for extra speed. * // Only set this config once, before you create any Viewers. * configs.doublePrecisionEnabled = false; * * // Create a Viewer, to which our configs apply * const viewer = new Viewer({ * canvasId: "myCanvas" * }); * * viewer.camera.eye = [-3.933, 2.855, 27.018]; * viewer.camera.look = [4.400, 3.724, 8.899]; * viewer.camera.up = [-0.018, 0.999, 0.039]; * * const xktLoader = new XKTLoaderPlugin(viewer); * * const model = xktLoader.load({ * src: "../assets/models/xkt/v8/ifc/Duplex.ifc.xkt" * }); * ```` */ class Configs { /** * Creates a Configs. */ constructor() { } /** * Sets whether double precision mode is enabled for Viewers. * * When double precision mode is enabled (default), Viewers will accurately render models that contain * double-precision coordinates, without jittering. * * Internally, double precision incurs extra performance and memory overhead, so if we're certain that * we're not going to render models that rely on double-precision coordinates, then it's a good idea to disable * it, especially on low-power devices. * * This should only be set once, before creating any Viewers. * * @returns {Boolean} */ set doublePrecisionEnabled(doublePrecision) { math.setDoublePrecisionEnabled(doublePrecision); } /** * Gets whether double precision mode is enabled for all Viewers. * * @returns {Boolean} */ get doublePrecisionEnabled() { return math.getDoublePrecisionEnabled(); } /** * Sets the maximum data texture height. * * Should be a multiple of 1024. Default is 4096, which is the maximum allowed value. */ set maxDataTextureHeight(value) { value = Math.ceil(value / 1024) * 1024; if (value > 4096) { value = 4096; } else if (value < 1024) { value = 1024; } maxDataTextureHeight = value; } /** * Sets maximum data texture height. * @returns {*|number} */ get maxDataTextureHeight() { return maxDataTextureHeight; } /** * Sets the maximum batched geometry VBO size. * * Default value is 5000000, which is the maximum size. * * Minimum size is 100000. */ set maxGeometryBatchSize(value) { if (value < 100000) { value = 100000; } else if (value > 5000000) { value = 5000000; } maxGeometryBatchSize = value; } /** * Gets the maximum batched geometry VBO size. */ get maxGeometryBatchSize() { return maxGeometryBatchSize; } } const configs$2 = new Configs(); /** * @private */ class VBOBatchingTrianglesBuffer { constructor(maxBatchSize = configs$2.maxGeometryBatchSize) { this.maxVerts = maxBatchSize; this.maxIndices = maxBatchSize * 3; // Rough rule-of-thumb this.positions = []; this.colors = []; this.uv = []; this.metallicRoughness = []; this.normals = []; this.pickColors = []; this.offsets = []; this.indices = []; this.edgeIndices = []; } } const translate$1 = math.mat4(); const scale$1 = math.mat4(); /** * @private */ function quantizePositions(positions, aabb, positionsDecodeMatrix) { // http://cg.postech.ac.kr/research/mesh_comp_mobile/mesh_comp_mobile_conference.pdf const lenPositions = positions.length; const quantizedPositions = new Uint16Array(lenPositions); const xmin = aabb[0]; const ymin = aabb[1]; const zmin = aabb[2]; const xwid = aabb[3] - xmin; const ywid = aabb[4] - ymin; const zwid = aabb[5] - zmin; const maxInt = 65525; const xMultiplier = maxInt / xwid; const yMultiplier = maxInt / ywid; const zMultiplier = maxInt / zwid; const verify = (num) => num >= 0 ? num : 0; for (let i = 0; i < lenPositions; i += 3) { quantizedPositions[i + 0] = Math.floor(verify(positions[i + 0] - xmin) * xMultiplier); quantizedPositions[i + 1] = Math.floor(verify(positions[i + 1] - ymin) * yMultiplier); quantizedPositions[i + 2] = Math.floor(verify(positions[i + 2] - zmin) * zMultiplier); } math.identityMat4(translate$1); math.translationMat4v(aabb, translate$1); math.identityMat4(scale$1); math.scalingMat4v([xwid / maxInt, ywid / maxInt, zwid / maxInt], scale$1); math.mulMat4(translate$1, scale$1, positionsDecodeMatrix); return quantizedPositions; } /** * @private * @param aabb * @param positionsDecodeMatrix * @returns {*} */ function createPositionsDecodeMatrix(aabb, positionsDecodeMatrix) { // http://cg.postech.ac.kr/research/mesh_comp_mobile/mesh_comp_mobile_conference.pdf const xmin = aabb[0]; const ymin = aabb[1]; const zmin = aabb[2]; const xwid = aabb[3] - xmin; const ywid = aabb[4] - ymin; const zwid = aabb[5] - zmin; const maxInt = 65525; math.identityMat4(translate$1); math.translationMat4v(aabb, translate$1); math.identityMat4(scale$1); math.scalingMat4v([xwid / maxInt, ywid / maxInt, zwid / maxInt], scale$1); math.mulMat4(translate$1, scale$1, positionsDecodeMatrix); return positionsDecodeMatrix; } /** * @private */ function transformAndOctEncodeNormals(worldNormalMatrix, normals, lenNormals, compressedNormals, lenCompressedNormals) { // http://jcgt.org/published/0003/02/01/ function dot(array, vec3) { return array[0] * vec3[0] + array[1] * vec3[1] + array[2] * vec3[2]; } let oct, dec, best, currentCos, bestCos; let i; let localNormal = new Float32Array([0, 0, 0, 0]); let worldNormal = new Float32Array([0, 0, 0, 0]); for (i = 0; i < lenNormals; i += 3) { localNormal[0] = normals[i]; localNormal[1] = normals[i + 1]; localNormal[2] = normals[i + 2]; math.transformVec3(worldNormalMatrix, localNormal, worldNormal); math.normalizeVec3(worldNormal, worldNormal); // Test various combinations of ceil and floor to minimize rounding errors best = oct = octEncodeVec3(worldNormal, "floor", "floor"); dec = octDecodeVec2(oct); currentCos = bestCos = dot(worldNormal, dec); oct = octEncodeVec3(worldNormal, "ceil", "floor"); dec = octDecodeVec2(oct); currentCos = dot(worldNormal, dec); if (currentCos > bestCos) { best = oct; bestCos = currentCos; } oct = octEncodeVec3(worldNormal, "floor", "ceil"); dec = octDecodeVec2(oct); currentCos = dot(worldNormal, dec); if (currentCos > bestCos) { best = oct; bestCos = currentCos; } oct = octEncodeVec3(worldNormal, "ceil", "ceil"); dec = octDecodeVec2(oct); currentCos = dot(worldNormal, dec); if (currentCos > bestCos) { best = oct; bestCos = currentCos; } compressedNormals[lenCompressedNormals + i + 0] = best[0]; compressedNormals[lenCompressedNormals + i + 1] = best[1]; compressedNormals[lenCompressedNormals + i + 2] = 0.0; // Unused } lenCompressedNormals += lenNormals; return lenCompressedNormals; } /** * @private */ function octEncodeVec3(p, xfunc, yfunc) { // Oct-encode single normal vector in 2 bytes let x = p[0] / (Math.abs(p[0]) + Math.abs(p[1]) + Math.abs(p[2])); let y = p[1] / (Math.abs(p[0]) + Math.abs(p[1]) + Math.abs(p[2])); if (p[2] < 0) { let tempx = x; let tempy = y; tempx = (1 - Math.abs(y)) * (x >= 0 ? 1 : -1); tempy = (1 - Math.abs(x)) * (y >= 0 ? 1 : -1); x = tempx; y = tempy; } return new Int8Array([ Math[xfunc](x * 127.5 + (x < 0 ? -1 : 0)), Math[yfunc](y * 127.5 + (y < 0 ? -1 : 0)) ]); } /** * @private */ function octDecodeVec2(oct) { // Decode an oct-encoded normal let x = oct[0]; let y = oct[1]; x /= x < 0 ? 127 : 128; y /= y < 0 ? 127 : 128; const z = 1 - Math.abs(x) - Math.abs(y); if (z < 0) { x = (1 - Math.abs(y)) * (x >= 0 ? 1 : -1); y = (1 - Math.abs(x)) * (y >= 0 ? 1 : -1); } const length = Math.sqrt(x * x + y * y + z * z); return [ x / length, y / length, z / length ]; } const tempMat4$1 = math.mat4(); const tempMat4b = math.mat4(); const tempVec4a$d = math.vec4([0, 0, 0, 1]); const tempVec3a$C = math.vec3(); const tempVec3b$y = math.vec3(); const tempVec3c$t = math.vec3(); const tempVec3d$c = math.vec3(); const tempVec3e$2 = math.vec3(); const tempVec3f$2 = math.vec3(); const tempVec3g$1 = math.vec3(); /** * @private */ class VBOBatchingTrianglesLayer { /** * @param model * @param cfg.model * @param cfg.autoNormals * @param cfg.layerIndex * @param cfg.positionsDecodeMatrix * @param cfg.uvDecodeMatrix * @param cfg.maxGeometryBatchSize * @param cfg.origin * @param cfg.scratchMemory * @param cfg.textureSet * @param cfg.solid */ constructor(cfg) { // console.info("Creating VBOBatchingTrianglesLayer"); /** * Owner model * @type {VBOSceneModel} */ this.model = cfg.model; /** * State sorting key. * @type {string} */ this.sortId = "TrianglesBatchingLayer" + (cfg.solid ? "-solid" : "-surface") + (cfg.autoNormals ? "-autonormals" : "-normals") // TODO: These two parts need to be IDs (ie. unique): + (cfg.textureSet && cfg.textureSet.colorTexture ? "-colorTexture" : "") + (cfg.textureSet && cfg.textureSet.metallicRoughnessTexture ? "-metallicRoughnessTexture" : ""); /** * Index of this TrianglesBatchingLayer in {@link VBOSceneModel#_layerList}. * @type {Number} */ this.layerIndex = cfg.layerIndex; this._renderers = getRenderers$7(cfg.model.scene); this._buffer = new VBOBatchingTrianglesBuffer(cfg.maxGeometryBatchSize); this._scratchMemory = cfg.scratchMemory; this._state = new RenderState({ origin: math.vec3(), positionsBuf: null, offsetsBuf: null, normalsBuf: null, colorsBuf: null, uvBuf: null, metallicRoughnessBuf: null, flagsBuf: null, indicesBuf: null, edgeIndicesBuf: null, positionsDecodeMatrix: null, uvDecodeMatrix: null, textureSet: cfg.textureSet, pbrSupported: false // Set in #finalize if we have enough to support quality rendering }); // These counts are used to avoid unnecessary render passes this._numPortions = 0; this._numVisibleLayerPortions = 0; this._numTransparentLayerPortions = 0; this._numXRayedLayerPortions = 0; this._numSelectedLayerPortions = 0; this._numHighlightedLayerPortions = 0; this._numClippableLayerPortions = 0; this._numEdgesLayerPortions = 0; this._numPickableLayerPortions = 0; this._numCulledLayerPortions = 0; this._modelAABB = math.collapseAABB3(); // Model-space AABB this._portions = []; this._meshes = []; this._numVerts = 0; this._aabb = math.collapseAABB3(); this.aabbDirty = true; this._finalized = false; if (cfg.positionsDecodeMatrix) { this._state.positionsDecodeMatrix = math.mat4(cfg.positionsDecodeMatrix); } if (cfg.uvDecodeMatrix) { this._state.uvDecodeMatrix = math.mat3(cfg.uvDecodeMatrix); this._preCompressedUVsExpected = true; } else { this._preCompressedUVsExpected = false; } if (cfg.origin) { this._state.origin.set(cfg.origin); } /** * When true, this layer contains solid triangle meshes, otherwise this layer contains surface triangle meshes * @type {boolean} */ this.solid = !!cfg.solid; /** * The type of primitives in this layer. */ this.primitive = cfg.primitive; } get aabb() { if (this.aabbDirty) { math.collapseAABB3(this._aabb); for (let i = 0, len = this._meshes.length; i < len; i++) { math.expandAABB3(this._aabb, this._meshes[i].aabb); } this.aabbDirty = false; } return this._aabb; } /** * Tests if there is room for another portion in this TrianglesBatchingLayer. * * @param lenPositions Number of positions we'd like to create in the portion. * @param lenIndices Number of indices we'd like to create in this portion. * @returns {Boolean} True if OK to create another portion. */ canCreatePortion(lenPositions, lenIndices) { if (this._finalized) { throw "Already finalized"; } return ((this._buffer.positions.length + lenPositions) < (this._buffer.maxVerts * 3) && (this._buffer.indices.length + lenIndices) < (this._buffer.maxIndices)); } /** * Creates a new portion within this TrianglesBatchingLayer, returns the new portion ID. * * Gives the portion the specified geometry, color and matrix. * * @param mesh The SceneModelMesh that owns the portion * @param cfg.positions Flat float Local-space positions array. * @param cfg.positionsCompressed Flat quantized positions array - decompressed with TrianglesBatchingLayer positionsDecodeMatrix * @param [cfg.normals] Flat float normals array. * @param [cfg.uv] Flat UVs array. * @param [cfg.uvCompressed] * @param [cfg.colors] Flat float colors array. * @param [cfg.colorsCompressed] * @param cfg.indices Flat int indices array. * @param [cfg.edgeIndices] Flat int edges indices array. * @param cfg.color Quantized RGB color [0..255,0..255,0..255,0..255] * @param cfg.metallic Metalness factor [0..255] * @param cfg.roughness Roughness factor [0..255] * @param cfg.opacity Opacity [0..255] * @param [cfg.meshMatrix] Flat float 4x4 matrix * @param cfg.aabb Flat float AABB World-space AABB * @param cfg.pickColor Quantized pick color * @returns {number} Portion ID */ createPortion(mesh, cfg) { if (this._finalized) { throw "Already finalized"; } const positions = cfg.positions; const positionsCompressed = cfg.positionsCompressed; const normals = cfg.normals; const normalsCompressed = cfg.normalsCompressed; const uv = cfg.uv; const uvCompressed = cfg.uvCompressed; const colors = cfg.colors; const colorsCompressed = cfg.colorsCompressed; const indices = cfg.indices; const edgeIndices = cfg.edgeIndices; const color = cfg.color; const metallic = cfg.metallic; const roughness = cfg.roughness; const opacity = cfg.opacity; const meshMatrix = cfg.meshMatrix; const pickColor = cfg.pickColor; const scene = this.model.scene; const buffer = this._buffer; const vertsBaseIndex = buffer.positions.length / 3; let numVerts; math.expandAABB3(this._modelAABB, cfg.aabb); if (this._state.positionsDecodeMatrix) { if (!positionsCompressed) { throw "positionsCompressed expected"; } numVerts = positionsCompressed.length / 3; for (let i = 0, len = positionsCompressed.length; i < len; i++) { buffer.positions.push(positionsCompressed[i]); } } else { if (!positions) { throw "positions expected"; } numVerts = positions.length / 3; for (let i = 0, len = positions.length; i < len; i++) { buffer.positions.push(positions[i]); } } if (normalsCompressed && normalsCompressed.length > 0) { for (let i = 0, len = normalsCompressed.length; i < len; i++) { buffer.normals.push(normalsCompressed[i]); } } else if (normals && normals.length > 0) { const worldNormalMatrix = tempMat4$1; if (meshMatrix) { math.inverseMat4(math.transposeMat4(meshMatrix, tempMat4b), worldNormalMatrix); // Note: order of inverse and transpose doesn't matter } else { math.identityMat4(worldNormalMatrix, worldNormalMatrix); } transformAndOctEncodeNormals(worldNormalMatrix, normals, normals.length, buffer.normals, buffer.normals.length); } if (colors) { for (let i = 0, len = colors.length; i < len; i += 3) { buffer.colors.push(colors[i] * 255); buffer.colors.push(colors[i + 1] * 255); buffer.colors.push(colors[i + 2] * 255); buffer.colors.push(255); } } else if (colorsCompressed) { for (let i = 0, len = colors.length; i < len; i += 3) { buffer.colors.push(colors[i]); buffer.colors.push(colors[i + 1]); buffer.colors.push(colors[i + 2]); buffer.colors.push(255); } } else if (color) { const r = color[0]; // Color is pre-quantized by VBOSceneModel const g = color[1]; const b = color[2]; const a = opacity; for (let i = 0; i < numVerts; i++) { buffer.colors.push(r); buffer.colors.push(g); buffer.colors.push(b); buffer.colors.push(a); } } const metallicValue = (metallic !== null && metallic !== undefined) ? metallic : 0; const roughnessValue = (roughness !== null && roughness !== undefined) ? roughness : 255; for (let i = 0; i < numVerts; i++) { buffer.metallicRoughness.push(metallicValue); buffer.metallicRoughness.push(roughnessValue); } if (uv && uv.length > 0) { for (let i = 0, len = uv.length; i < len; i++) { buffer.uv.push(uv[i]); } } else if (uvCompressed && uvCompressed.length > 0) { for (let i = 0, len = uvCompressed.length; i < len; i++) { buffer.uv.push(uvCompressed[i]); } } for (let i = 0, len = indices.length; i < len; i++) { buffer.indices.push(vertsBaseIndex + indices[i]); } if (edgeIndices) { for (let i = 0, len = edgeIndices.length; i < len; i++) { buffer.edgeIndices.push(vertsBaseIndex + edgeIndices[i]); } } { const pickColorsBase = buffer.pickColors.length; const lenPickColors = numVerts * 4; for (let i = pickColorsBase, len = pickColorsBase + lenPickColors; i < len; i += 4) { buffer.pickColors.push(pickColor[0]); buffer.pickColors.push(pickColor[1]); buffer.pickColors.push(pickColor[2]); buffer.pickColors.push(pickColor[3]); } } if (scene.entityOffsetsEnabled) { for (let i = 0; i < numVerts; i++) { buffer.offsets.push(0); buffer.offsets.push(0); buffer.offsets.push(0); } } const portionId = this._portions.length; const portion = { vertsBaseIndex: vertsBaseIndex, numVerts: numVerts, indicesBaseIndex: buffer.indices.length - indices.length, numIndices: indices.length, }; if (scene.readableGeometryEnabled) { // Quantized in-memory positions are initialized in finalize() portion.indices = indices; if (scene.entityOffsetsEnabled) { portion.offset = new Float32Array(3); } } this._portions.push(portion); this._numPortions++; this.model.numPortions++; this._numVerts += portion.numVerts; this._meshes.push(mesh); return portionId; } /** * Builds batch VBOs from appended geometries. * No more portions can then be created. */ finalize() { if (this._finalized) { return; } const state = this._state; const gl = this.model.scene.canvas.gl; const buffer = this._buffer; if (buffer.positions.length > 0) { const quantizedPositions = (this._state.positionsDecodeMatrix) ? new Uint16Array(buffer.positions) : quantizePositions(buffer.positions, this._modelAABB, this._state.positionsDecodeMatrix = math.mat4()); // BOTTLENECK state.positionsBuf = new ArrayBuf(gl, gl.ARRAY_BUFFER, quantizedPositions, quantizedPositions.length, 3, gl.STATIC_DRAW); if (this.model.scene.readableGeometryEnabled) { for (let i = 0, numPortions = this._portions.length; i < numPortions; i++) { const portion = this._portions[i]; const start = portion.vertsBaseIndex * 3; const end = start + (portion.numVerts * 3); portion.quantizedPositions = quantizedPositions.slice(start, end); } } } if (buffer.normals.length > 0) { // Normals are already oct-encoded const normals = new Int8Array(buffer.normals); let normalized = true; // For oct encoded UInts state.normalsBuf = new ArrayBuf(gl, gl.ARRAY_BUFFER, normals, buffer.normals.length, 3, gl.STATIC_DRAW, normalized); } if (buffer.colors.length > 0) { // Colors are already compressed const colors = new Uint8Array(buffer.colors); let normalized = false; state.colorsBuf = new ArrayBuf(gl, gl.ARRAY_BUFFER, colors, buffer.colors.length, 4, gl.DYNAMIC_DRAW, normalized); } if (buffer.uv.length > 0) { if (!state.uvDecodeMatrix) { const bounds = geometryCompressionUtils.getUVBounds(buffer.uv); const result = geometryCompressionUtils.compressUVs(buffer.uv, bounds.min, bounds.max); const uv = result.quantized; let notNormalized = false; state.uvDecodeMatrix = math.mat3(result.decodeMatrix); state.uvBuf = new ArrayBuf(gl, gl.ARRAY_BUFFER, uv, uv.length, 2, gl.STATIC_DRAW, notNormalized); } else { let notNormalized = false; state.uvBuf = new ArrayBuf(gl, gl.ARRAY_BUFFER, buffer.uv, buffer.uv.length, 2, gl.STATIC_DRAW, notNormalized); } } if (buffer.metallicRoughness.length > 0) { const metallicRoughness = new Uint8Array(buffer.metallicRoughness); let normalized = false; state.metallicRoughnessBuf = new ArrayBuf(gl, gl.ARRAY_BUFFER, metallicRoughness, buffer.metallicRoughness.length, 2, gl.STATIC_DRAW, normalized); } if (buffer.positions.length > 0) { // Because we build flags arrays here, get their length from the positions array const flagsLength = (buffer.positions.length / 3); const flags = new Float32Array(flagsLength); const notNormalized = false; state.flagsBuf = new ArrayBuf(gl, gl.ARRAY_BUFFER, flags, flags.length, 1, gl.DYNAMIC_DRAW, notNormalized); } if (buffer.pickColors.length > 0) { const pickColors = new Uint8Array(buffer.pickColors); let normalized = false; state.pickColorsBuf = new ArrayBuf(gl, gl.ARRAY_BUFFER, pickColors, buffer.pickColors.length, 4, gl.STATIC_DRAW, normalized); } if (this.model.scene.entityOffsetsEnabled) { if (buffer.offsets.length > 0) { const offsets = new Float32Array(buffer.offsets); state.offsetsBuf = new ArrayBuf(gl, gl.ARRAY_BUFFER, offsets, buffer.offsets.length, 3, gl.DYNAMIC_DRAW); } } if (buffer.indices.length > 0) { const indices = new Uint32Array(buffer.indices); state.indicesBuf = new ArrayBuf(gl, gl.ELEMENT_ARRAY_BUFFER, indices, buffer.indices.length, 1, gl.STATIC_DRAW); } if (buffer.edgeIndices.length > 0) { const edgeIndices = new Uint32Array(buffer.edgeIndices); state.edgeIndicesBuf = new ArrayBuf(gl, gl.ELEMENT_ARRAY_BUFFER, edgeIndices, buffer.edgeIndices.length, 1, gl.STATIC_DRAW); } this._state.pbrSupported = !!state.metallicRoughnessBuf && !!state.uvBuf && !!state.normalsBuf && !!state.textureSet && !!state.textureSet.colorTexture && !!state.textureSet.metallicRoughnessTexture; this._state.colorTextureSupported = !!state.uvBuf && !!state.textureSet && !!state.textureSet.colorTexture; this._buffer = null; this._finalized = true; } isEmpty() { return (!this._state.indicesBuf); } initFlags(portionId, flags, meshTransparent) { if (flags & ENTITY_FLAGS.VISIBLE) { this._numVisibleLayerPortions++; this.model.numVisibleLayerPortions++; } if (flags & ENTITY_FLAGS.HIGHLIGHTED) { this._numHighlightedLayerPortions++; this.model.numHighlightedLayerPortions++; } if (flags & ENTITY_FLAGS.XRAYED) { this._numXRayedLayerPortions++; this.model.numXRayedLayerPortions++; } if (flags & ENTITY_FLAGS.SELECTED) { this._numSelectedLayerPortions++; this.model.numSelectedLayerPortions++; } if (flags & ENTITY_FLAGS.CLIPPABLE) { this._numClippableLayerPortions++; this.model.numClippableLayerPortions++; } if (flags & ENTITY_FLAGS.EDGES) { this._numEdgesLayerPortions++; this.model.numEdgesLayerPortions++; } if (flags & ENTITY_FLAGS.PICKABLE) { this._numPickableLayerPortions++; this.model.numPickableLayerPortions++; } if (flags & ENTITY_FLAGS.CULLED) { this._numCulledLayerPortions++; this.model.numCulledLayerPortions++; } if (meshTransparent) { this._numTransparentLayerPortions++; this.model.numTransparentLayerPortions++; } const deferred = true; this._setFlags(portionId, flags, meshTransparent, deferred); } flushInitFlags() { this._setDeferredFlags(); } setVisible(portionId, flags, transparent) { if (!this._finalized) { throw "Not finalized"; } if (flags & ENTITY_FLAGS.VISIBLE) { this._numVisibleLayerPortions++; this.model.numVisibleLayerPortions++; } else { this._numVisibleLayerPortions--; this.model.numVisibleLayerPortions--; } this._setFlags(portionId, flags, transparent); } setHighlighted(portionId, flags, transparent) { if (!this._finalized) { throw "Not finalized"; } if (flags & ENTITY_FLAGS.HIGHLIGHTED) { this._numHighlightedLayerPortions++; this.model.numHighlightedLayerPortions++; } else { this._numHighlightedLayerPortions--; this.model.numHighlightedLayerPortions--; } this._setFlags(portionId, flags, transparent); } setXRayed(portionId, flags, transparent) { if (!this._finalized) { throw "Not finalized"; } if (flags & ENTITY_FLAGS.XRAYED) { this._numXRayedLayerPortions++; this.model.numXRayedLayerPortions++; } else { this._numXRayedLayerPortions--; this.model.numXRayedLayerPortions--; } this._setFlags(portionId, flags, transparent); } setSelected(portionId, flags, transparent) { if (!this._finalized) { throw "Not finalized"; } if (flags & ENTITY_FLAGS.SELECTED) { this._numSelectedLayerPortions++; this.model.numSelectedLayerPortions++; } else { this._numSelectedLayerPortions--; this.model.numSelectedLayerPortions--; } this._setFlags(portionId, flags, transparent); } setEdges(portionId, flags, transparent) { if (!this._finalized) { throw "Not finalized"; } if (flags & ENTITY_FLAGS.EDGES) { this._numEdgesLayerPortions++; this.model.numEdgesLayerPortions++; } else { this._numEdgesLayerPortions--; this.model.numEdgesLayerPortions--; } this._setFlags(portionId, flags, transparent); } setClippable(portionId, flags) { if (!this._finalized) { throw "Not finalized"; } if (flags & ENTITY_FLAGS.CLIPPABLE) { this._numClippableLayerPortions++; this.model.numClippableLayerPortions++; } else { this._numClippableLayerPortions--; this.model.numClippableLayerPortions--; } this._setFlags(portionId, flags); } setCulled(portionId, flags, transparent) { if (!this._finalized) { throw "Not finalized"; } if (flags & ENTITY_FLAGS.CULLED) { this._numCulledLayerPortions++; this.model.numCulledLayerPortions++; } else { this._numCulledLayerPortions--; this.model.numCulledLayerPortions--; } this._setFlags(portionId, flags, transparent); } setCollidable(portionId, flags) { if (!this._finalized) { throw "Not finalized"; } } setPickable(portionId, flags, transparent) { if (!this._finalized) { throw "Not finalized"; } if (flags & ENTITY_FLAGS.PICKABLE) { this._numPickableLayerPortions++; this.model.numPickableLayerPortions++; } else { this._numPickableLayerPortions--; this.model.numPickableLayerPortions--; } this._setFlags(portionId, flags, transparent); } setColor(portionId, color) { if (!this._finalized) { throw "Not finalized"; } const portionsIdx = portionId; const portion = this._portions[portionsIdx]; const vertsBaseIndex = portion.vertsBaseIndex; const numVerts = portion.numVerts; const firstColor = vertsBaseIndex * 4; const lenColor = numVerts * 4; const tempArray = this._scratchMemory.getUInt8Array(lenColor); const r = color[0]; const g = color[1]; const b = color[2]; const a = color[3]; for (let i = 0; i < lenColor; i += 4) { tempArray[i + 0] = r; tempArray[i + 1] = g; tempArray[i + 2] = b; tempArray[i + 3] = a; } if (this._state.colorsBuf) { this._state.colorsBuf.setData(tempArray, firstColor, lenColor); } } setTransparent(portionId, flags, transparent) { if (transparent) { this._numTransparentLayerPortions++; this.model.numTransparentLayerPortions++; } else { this._numTransparentLayerPortions--; this.model.numTransparentLayerPortions--; } this._setFlags(portionId, flags, transparent); } /** * flags are 4bits values encoded on a 32bit base. color flag on the first 4 bits, silhouette flag on the next 4 bits and so on for edge, pick and clippable. */ _setFlags(portionId, flags, transparent, deferred = false) { if (!this._finalized) { throw "Not finalized"; } const portionsIdx = portionId; const portion = this._portions[portionsIdx]; const vertsBaseIndex = portion.vertsBaseIndex; const numVerts = portion.numVerts; const firstFlag = vertsBaseIndex; const lenFlags = numVerts; const visible = !!(flags & ENTITY_FLAGS.VISIBLE); const xrayed = !!(flags & ENTITY_FLAGS.XRAYED); const highlighted = !!(flags & ENTITY_FLAGS.HIGHLIGHTED); const selected = !!(flags & ENTITY_FLAGS.SELECTED); const edges = !!(flags & ENTITY_FLAGS.EDGES); const pickable = !!(flags & ENTITY_FLAGS.PICKABLE); const culled = !!(flags & ENTITY_FLAGS.CULLED); let colorFlag; if (!visible || culled || xrayed || (highlighted && !this.model.scene.highlightMaterial.glowThrough) || (selected && !this.model.scene.selectedMaterial.glowThrough)) { colorFlag = RENDER_PASSES.NOT_RENDERED; } else { if (transparent) { colorFlag = RENDER_PASSES.COLOR_TRANSPARENT; } else { colorFlag = RENDER_PASSES.COLOR_OPAQUE; } } let silhouetteFlag; if (!visible || culled) { silhouetteFlag = RENDER_PASSES.NOT_RENDERED; } else if (selected) { silhouetteFlag = RENDER_PASSES.SILHOUETTE_SELECTED; } else if (highlighted) { silhouetteFlag = RENDER_PASSES.SILHOUETTE_HIGHLIGHTED; } else if (xrayed) { silhouetteFlag = RENDER_PASSES.SILHOUETTE_XRAYED; } else { silhouetteFlag = RENDER_PASSES.NOT_RENDERED; } let edgeFlag = 0; if (!visible || culled) { edgeFlag = RENDER_PASSES.NOT_RENDERED; } else if (selected) { edgeFlag = RENDER_PASSES.EDGES_SELECTED; } else if (highlighted) { edgeFlag = RENDER_PASSES.EDGES_HIGHLIGHTED; } else if (xrayed) { edgeFlag = RENDER_PASSES.EDGES_XRAYED; } else if (edges) { if (transparent) { edgeFlag = RENDER_PASSES.EDGES_COLOR_TRANSPARENT; } else { edgeFlag = RENDER_PASSES.EDGES_COLOR_OPAQUE; } } else { edgeFlag = RENDER_PASSES.NOT_RENDERED; } let pickFlag = (visible && !culled && pickable) ? RENDER_PASSES.PICK : RENDER_PASSES.NOT_RENDERED; const clippableFlag = !!(flags & ENTITY_FLAGS.CLIPPABLE) ? 1 : 0; if (deferred) { // Avoid zillions of individual WebGL bufferSubData calls - buffer them to apply in one shot if (!this._deferredFlagValues) { this._deferredFlagValues = new Float32Array(this._numVerts); } for (let i = firstFlag, len = (firstFlag + lenFlags); i < len; i++) { let vertFlag = 0; vertFlag |= colorFlag; vertFlag |= silhouetteFlag << 4; vertFlag |= edgeFlag << 8; vertFlag |= pickFlag << 12; vertFlag |= clippableFlag << 16; this._deferredFlagValues[i] = vertFlag; } } else if (this._state.flagsBuf) { const tempArray = this._scratchMemory.getFloat32Array(lenFlags); for (let i = 0; i < lenFlags; i++) { let vertFlag = 0; vertFlag |= colorFlag; vertFlag |= silhouetteFlag << 4; vertFlag |= edgeFlag << 8; vertFlag |= pickFlag << 12; vertFlag |= clippableFlag << 16; tempArray[i] = vertFlag; } this._state.flagsBuf.setData(tempArray, firstFlag, lenFlags); } } _setDeferredFlags() { if (this._deferredFlagValues) { this._state.flagsBuf.setData(this._deferredFlagValues); this._deferredFlagValues = null; } } setOffset(portionId, offset) { if (!this._finalized) { throw "Not finalized"; } if (!this.model.scene.entityOffsetsEnabled) { this.model.error("Entity#offset not enabled for this Viewer"); // See Viewer entityOffsetsEnabled return; } const portionsIdx = portionId; const portion = this._portions[portionsIdx]; const vertsBaseIndex = portion.vertsBaseIndex; const numVerts = portion.numVerts; const firstOffset = vertsBaseIndex * 3; const lenOffsets = numVerts * 3; const tempArray = this._scratchMemory.getFloat32Array(lenOffsets); const x = offset[0]; const y = offset[1]; const z = offset[2]; for (let i = 0; i < lenOffsets; i += 3) { tempArray[i + 0] = x; tempArray[i + 1] = y; tempArray[i + 2] = z; } if (this._state.offsetsBuf) { this._state.offsetsBuf.setData(tempArray, firstOffset, lenOffsets); } if (this.model.scene.readableGeometryEnabled) { portion.offset[0] = offset[0]; portion.offset[1] = offset[1]; portion.offset[2] = offset[2]; } } getEachVertex(portionId, callback) { if (!this.model.scene.readableGeometryEnabled) { return; } const state = this._state; const portion = this._portions[portionId]; if (!portion) { this.model.error("portion not found: " + portionId); return; } const positions = portion.quantizedPositions; const sceneModelMatrix = this.model.matrix; const origin = math.vec4(); origin.set(state.origin, 0); origin[3] = 1; math.mulMat4v4(sceneModelMatrix, origin, origin); const offsetX = origin[0]; const offsetY = origin[1]; const offsetZ = origin[2]; const worldPos = tempVec4a$d; const positionsDecodeMatrix = state.positionsDecodeMatrix; for (let i = 0, len = positions.length; i < len; i += 3) { worldPos[0] = positions[i]; worldPos[1] = positions[i + 1]; worldPos[2] = positions[i + 2]; worldPos[3] = 1.0; math.decompressPosition(worldPos, positionsDecodeMatrix); worldPos[3] = 1; math.mulMat4v4(sceneModelMatrix, worldPos, worldPos); worldPos[0] += offsetX; worldPos[1] += offsetY; worldPos[2] += offsetZ; callback(worldPos); } } getEachIndex(portionId, callback) { if (!this.model.scene.readableGeometryEnabled) { return; } const portion = this._portions[portionId]; if (!portion) { this.model.error("portion not found: " + portionId); return; } const indices = portion.indices; for (let i = 0, len = indices.length; i < len; i++) { callback(indices[i]); } } getElementsCountAndOffset(portionId) { let count = null; let offset = null; const portion = this._portions[portionId]; if (portion) { count = portion.numIndices; offset = portion.indicesBaseIndex; } return {count, offset} } readGeometryData(portionId) { if (!this._finalized) { throw "Not finalized"; } const portion = this._portions[portionId]; const state = this._state; const sceneModelMatrix = this.model.matrix; const positionsDecodeMatrix = state.positionsDecodeMatrix; const origin = math.vec4(); origin.set(state.origin, 0); origin[3] = 1; math.mulMat4v4(sceneModelMatrix, origin, origin); const indices = state.indicesBuf.getData( portion.indicesBaseIndex, portion.numIndices ).map(i => i - portion.vertsBaseIndex); const matrix = math.mulMat4( sceneModelMatrix, positionsDecodeMatrix, new Array(16) ); matrix[12] += origin[0]; matrix[13] += origin[1]; matrix[14] += origin[2]; const positionsQuantized = state.positionsBuf.getData( portion.vertsBaseIndex, portion.numVerts ); const positions = math.transformPositions3( matrix, positionsQuantized, new Array(positionsQuantized.length) ); // const aabb = math.positions3ToAABB3(positions); // console.log({aabbToniBatching: aabb}); return { indices, positions }; } // ---------------------- COLOR RENDERING ----------------------------------- drawColorOpaque(renderFlags, frameCtx) { if (this._numCulledLayerPortions === this._numPortions || this._numVisibleLayerPortions === 0 || this._numTransparentLayerPortions === this._numPortions || this._numXRayedLayerPortions === this._numPortions) { return; } this._updateBackfaceCull(renderFlags, frameCtx); const useAlphaCutoff = this._state.textureSet && (typeof(this._state.textureSet.alphaCutoff) === "number"); if (frameCtx.withSAO && this.model.saoEnabled) { if (frameCtx.pbrEnabled && this.model.pbrEnabled && this._state.pbrSupported) { if (this._renderers.pbrRendererWithSAO) { this._renderers.pbrRendererWithSAO.drawLayer(frameCtx, this, RENDER_PASSES.COLOR_OPAQUE); } } else if (frameCtx.colorTextureEnabled && this.model.colorTextureEnabled && this._state.colorTextureSupported) { if (useAlphaCutoff) { if (this._renderers.colorTextureRendererWithSAOAlphaCutoff) { this._renderers.colorTextureRendererWithSAOAlphaCutoff.drawLayer(frameCtx, this, RENDER_PASSES.COLOR_OPAQUE); } } else { if (this._renderers.colorTextureRendererWithSAO) { this._renderers.colorTextureRendererWithSAO.drawLayer(frameCtx, this, RENDER_PASSES.COLOR_OPAQUE); } } } else if (this._state.normalsBuf) { if (this._renderers.colorRendererWithSAO) { this._renderers.colorRendererWithSAO.drawLayer(frameCtx, this, RENDER_PASSES.COLOR_OPAQUE); } } else { if (this._renderers.flatColorRendererWithSAO) { this._renderers.flatColorRendererWithSAO.drawLayer(frameCtx, this, RENDER_PASSES.COLOR_OPAQUE); } } } else { if (frameCtx.pbrEnabled && this.model.pbrEnabled && this._state.pbrSupported) { if (this._renderers.pbrRenderer) { this._renderers.pbrRenderer.drawLayer(frameCtx, this, RENDER_PASSES.COLOR_OPAQUE); } } else if (frameCtx.colorTextureEnabled && this.model.colorTextureEnabled && this._state.colorTextureSupported) { if (useAlphaCutoff) { if (this._renderers.colorTextureRendererAlphaCutoff) { this._renderers.colorTextureRendererAlphaCutoff.drawLayer(frameCtx, this, RENDER_PASSES.COLOR_OPAQUE); } } else { if (this._renderers.colorTextureRenderer) { this._renderers.colorTextureRenderer.drawLayer(frameCtx, this, RENDER_PASSES.COLOR_OPAQUE); } } } else if (this._state.normalsBuf) { if (this._renderers.colorRenderer) { this._renderers.colorRenderer.drawLayer(frameCtx, this, RENDER_PASSES.COLOR_OPAQUE); } } else { if (this._renderers.flatColorRenderer) { this._renderers.flatColorRenderer.drawLayer(frameCtx, this, RENDER_PASSES.COLOR_OPAQUE); } } } } _updateBackfaceCull(renderFlags, frameCtx) { const backfaces = true; // See XCD-230 if (frameCtx.backfaces !== backfaces) { const gl = frameCtx.gl; { gl.disable(gl.CULL_FACE); } frameCtx.backfaces = backfaces; } } drawColorTransparent(renderFlags, frameCtx) { if (this._numCulledLayerPortions === this._numPortions || this._numVisibleLayerPortions === 0 || this._numTransparentLayerPortions === 0 || this._numXRayedLayerPortions === this._numPortions) { return; } this._updateBackfaceCull(renderFlags, frameCtx); if (frameCtx.pbrEnabled && this.model.pbrEnabled && this._state.pbrSupported) { if (this._renderers.pbrRenderer) { this._renderers.pbrRenderer.drawLayer(frameCtx, this, RENDER_PASSES.COLOR_TRANSPARENT); } } else if (frameCtx.colorTextureEnabled && this.model.colorTextureEnabled && this._state.colorTextureSupported) { const useAlphaCutoff = this._state.textureSet && (typeof(this._state.textureSet.alphaCutoff) === "number"); if (useAlphaCutoff) { if (this._renderers.colorTextureRendererAlphaCutoff) { this._renderers.colorTextureRendererAlphaCutoff.drawLayer(frameCtx, this, RENDER_PASSES.COLOR_TRANSPARENT); } } else { if (this._renderers.colorTextureRenderer) { this._renderers.colorTextureRenderer.drawLayer(frameCtx, this, RENDER_PASSES.COLOR_TRANSPARENT); } } } else if (this._state.normalsBuf) { if (this._renderers.colorRenderer) { this._renderers.colorRenderer.drawLayer(frameCtx, this, RENDER_PASSES.COLOR_TRANSPARENT); } } else { if (this._renderers.flatColorRenderer) { this._renderers.flatColorRenderer.drawLayer(frameCtx, this, RENDER_PASSES.COLOR_TRANSPARENT); } } } // ---------------------- RENDERING SAO POST EFFECT TARGETS -------------- drawDepth(renderFlags, frameCtx) { if (this._numCulledLayerPortions === this._numPortions || this._numVisibleLayerPortions === 0 || this._numTransparentLayerPortions === this._numPortions || this._numXRayedLayerPortions === this._numPortions) { return; } this._updateBackfaceCull(renderFlags, frameCtx); if (this._renderers.depthRenderer) { this._renderers.depthRenderer.drawLayer(frameCtx, this, RENDER_PASSES.COLOR_OPAQUE); // Assume whatever post-effect uses depth (eg SAO) does not apply to transparent objects } } drawNormals(renderFlags, frameCtx) { if (this._numCulledLayerPortions === this._numPortions || this._numVisibleLayerPortions === 0 || this._numTransparentLayerPortions === this._numPortions || this._numXRayedLayerPortions === this._numPortions) { return; } this._updateBackfaceCull(renderFlags, frameCtx); if (this._renderers.normalsRenderer) { this._renderers.normalsRenderer.drawLayer(frameCtx, this, RENDER_PASSES.COLOR_OPAQUE); // Assume whatever post-effect uses normals (eg SAO) does not apply to transparent objects } } // ---------------------- SILHOUETTE RENDERING ----------------------------------- drawSilhouetteXRayed(renderFlags, frameCtx) { if (this._numCulledLayerPortions === this._numPortions || this._numVisibleLayerPortions === 0 || this._numXRayedLayerPortions === 0) { return; } this._updateBackfaceCull(renderFlags, frameCtx); if (this._renderers.silhouetteRenderer) { this._renderers.silhouetteRenderer.drawLayer(frameCtx, this, RENDER_PASSES.SILHOUETTE_XRAYED); } } drawSilhouetteHighlighted(renderFlags, frameCtx) { if (this._numCulledLayerPortions === this._numPortions || this._numVisibleLayerPortions === 0 || this._numHighlightedLayerPortions === 0) { return; } this._updateBackfaceCull(renderFlags, frameCtx); if (this._renderers.silhouetteRenderer) { this._renderers.silhouetteRenderer.drawLayer(frameCtx, this, RENDER_PASSES.SILHOUETTE_HIGHLIGHTED); } } drawSilhouetteSelected(renderFlags, frameCtx) { if (this._numCulledLayerPortions === this._numPortions || this._numVisibleLayerPortions === 0 || this._numSelectedLayerPortions === 0) { return; } this._updateBackfaceCull(renderFlags, frameCtx); if (this._renderers.silhouetteRenderer) { this._renderers.silhouetteRenderer.drawLayer(frameCtx, this, RENDER_PASSES.SILHOUETTE_SELECTED); } } // ---------------------- EDGES RENDERING ----------------------------------- drawEdgesColorOpaque(renderFlags, frameCtx) { if (this._numCulledLayerPortions === this._numPortions || this._numVisibleLayerPortions === 0 || this._numEdgesLayerPortions === 0) { return; } if (this._renderers.edgesColorRenderer) { this._renderers.edgesColorRenderer.drawLayer(frameCtx, this, RENDER_PASSES.EDGES_COLOR_OPAQUE); } } drawEdgesColorTransparent(renderFlags, frameCtx) { if (this._numCulledLayerPortions === this._numPortions || this._numVisibleLayerPortions === 0 || this._numEdgesLayerPortions === 0 || this._numTransparentLayerPortions === 0) { return; } if (this._renderers.edgesColorRenderer) { this._renderers.edgesColorRenderer.drawLayer(frameCtx, this, RENDER_PASSES.EDGES_COLOR_TRANSPARENT); } } drawEdgesHighlighted(renderFlags, frameCtx) { if (this._numCulledLayerPortions === this._numPortions || this._numVisibleLayerPortions === 0 || this._numHighlightedLayerPortions === 0) { return; } if (this._renderers.edgesRenderer) { this._renderers.edgesRenderer.drawLayer(frameCtx, this, RENDER_PASSES.EDGES_HIGHLIGHTED); } } drawEdgesSelected(renderFlags, frameCtx) { if (this._numCulledLayerPortions === this._numPortions || this._numVisibleLayerPortions === 0 || this._numSelectedLayerPortions === 0) { return; } if (this._renderers.edgesRenderer) { this._renderers.edgesRenderer.drawLayer(frameCtx, this, RENDER_PASSES.EDGES_SELECTED); } } drawEdgesXRayed(renderFlags, frameCtx) { if (this._numCulledLayerPortions === this._numPortions || this._numVisibleLayerPortions === 0 || this._numXRayedLayerPortions === 0) { return; } if (this._renderers.edgesRenderer) { this._renderers.edgesRenderer.drawLayer(frameCtx, this, RENDER_PASSES.EDGES_XRAYED); } } // ---------------------- OCCLUSION CULL RENDERING ----------------------------------- drawOcclusion(renderFlags, frameCtx) { if (this._numCulledLayerPortions === this._numPortions || this._numVisibleLayerPortions === 0) { return; } this._updateBackfaceCull(renderFlags, frameCtx); if (this._renderers.occlusionRenderer) { this._renderers.occlusionRenderer.drawLayer(frameCtx, this, RENDER_PASSES.COLOR_OPAQUE); } } // ---------------------- SHADOW BUFFER RENDERING ----------------------------------- drawShadow(renderFlags, frameCtx) { if (this._numCulledLayerPortions === this._numPortions || this._numVisibleLayerPortions === 0) { return; } this._updateBackfaceCull(renderFlags, frameCtx); if (this._renderers.shadowRenderer) { this._renderers.shadowRenderer.drawLayer(frameCtx, this, RENDER_PASSES.COLOR_OPAQUE); } } //---- PICKING ---------------------------------------------------------------------------------------------------- drawPickMesh(renderFlags, frameCtx) { if (this._numCulledLayerPortions === this._numPortions || this._numVisibleLayerPortions === 0) { return; } this._updateBackfaceCull(renderFlags, frameCtx); if (this._renderers.pickMeshRenderer) { this._renderers.pickMeshRenderer.drawLayer(frameCtx, this, RENDER_PASSES.PICK); } } drawPickDepths(renderFlags, frameCtx) { if (this._numCulledLayerPortions === this._numPortions || this._numVisibleLayerPortions === 0) { return; } this._updateBackfaceCull(renderFlags, frameCtx); if (this._renderers.pickDepthRenderer) { this._renderers.pickDepthRenderer.drawLayer(frameCtx, this, RENDER_PASSES.PICK); } } drawPickNormals(renderFlags, frameCtx) { if (this._numCulledLayerPortions === this._numPortions || this._numVisibleLayerPortions === 0) { return; } this._updateBackfaceCull(renderFlags, frameCtx); //////////////////////////////////////////////////////////////////////////////////////////////////// // TODO // if (this._state.normalsBuf) { // if (this._renderers.pickNormalsRenderer) { // this._renderers.pickNormalsRenderer.drawLayer(frameCtx, this, RENDER_PASSES.PICK); // } //////////////////////////////////////////////////////////////////////////////////////////////////// // } else { if (this._renderers.pickNormalsFlatRenderer) { this._renderers.pickNormalsFlatRenderer.drawLayer(frameCtx, this, RENDER_PASSES.PICK); } // } } drawSnapInit(renderFlags, frameCtx) { if (this._numCulledLayerPortions === this._numPortions || this._numVisibleLayerPortions === 0) { return; } this._updateBackfaceCull(renderFlags, frameCtx); if (this._renderers.snapInitRenderer) { this._renderers.snapInitRenderer.drawLayer(frameCtx, this, RENDER_PASSES.PICK); } } drawSnap(renderFlags, frameCtx) { if (this._numCulledLayerPortions === this._numPortions || this._numVisibleLayerPortions === 0) { return; } this._updateBackfaceCull(renderFlags, frameCtx); if (this._renderers.snapRenderer) { this._renderers.snapRenderer.drawLayer(frameCtx, this, RENDER_PASSES.PICK); } } //------------------------------------------------------------------------------------------------ precisionRayPickSurface(portionId, worldRayOrigin, worldRayDir, worldSurfacePos, worldNormal) { if (!this.model.scene.readableGeometryEnabled) { return false; } const state = this._state; const portion = this._portions[portionId]; if (!portion) { this.model.error("portion not found: " + portionId); return false; } const positions = portion.quantizedPositions; const indices = portion.indices; const origin = state.origin; const offset = portion.offset; const rtcRayOrigin = tempVec3a$C; const rtcRayDir = tempVec3b$y; rtcRayOrigin.set(origin ? math.subVec3(worldRayOrigin, origin, tempVec3c$t) : worldRayOrigin); // World -> RTC rtcRayDir.set(worldRayDir); if (offset) { math.subVec3(rtcRayOrigin, offset); } math.transformRay(this.model.worldNormalMatrix, rtcRayOrigin, rtcRayDir, rtcRayOrigin, rtcRayDir); // RTC -> local const a = tempVec3d$c; const b = tempVec3e$2; const c = tempVec3f$2; let gotIntersect = false; let closestDist = 0; const closestIntersectPos = tempVec3g$1; for (let i = 0, len = indices.length; i < len; i += 3) { const ia = indices[i] * 3; const ib = indices[i + 1] * 3; const ic = indices[i + 2] * 3; a[0] = positions[ia]; a[1] = positions[ia + 1]; a[2] = positions[ia + 2]; b[0] = positions[ib]; b[1] = positions[ib + 1]; b[2] = positions[ib + 2]; c[0] = positions[ic]; c[1] = positions[ic + 1]; c[2] = positions[ic + 2]; math.decompressPosition(a, state.positionsDecodeMatrix); math.decompressPosition(b, state.positionsDecodeMatrix); math.decompressPosition(c, state.positionsDecodeMatrix); if (math.rayTriangleIntersect(rtcRayOrigin, rtcRayDir, a, b, c, closestIntersectPos)) { math.transformPoint3(this.model.worldMatrix, closestIntersectPos, closestIntersectPos); if (offset) { math.addVec3(closestIntersectPos, offset); } if (origin) { math.addVec3(closestIntersectPos, origin); } const dist = Math.abs(math.lenVec3(math.subVec3(closestIntersectPos, worldRayOrigin, []))); if (!gotIntersect || dist > closestDist) { closestDist = dist; worldSurfacePos.set(closestIntersectPos); if (worldNormal) { // Not that wasteful to eagerly compute - unlikely to hit >2 surfaces on most geometry math.triangleNormal(a, b, c, worldNormal); } gotIntersect = true; } } } if (gotIntersect && worldNormal) { math.transformVec3(this.model.worldNormalMatrix, worldNormal, worldNormal); math.normalizeVec3(worldNormal); } return gotIntersect; } // --------- destroy() { const state = this._state; if (state.positionsBuf) { state.positionsBuf.destroy(); state.positionsBuf = null; } if (state.offsetsBuf) { state.offsetsBuf.destroy(); state.offsetsBuf = null; } if (state.normalsBuf) { state.normalsBuf.destroy(); state.normalsBuf = null; } if (state.colorsBuf) { state.colorsBuf.destroy(); state.colorsBuf = null; } if (state.metallicRoughnessBuf) { state.metallicRoughnessBuf.destroy(); state.metallicRoughnessBuf = null; } if (state.flagsBuf) { state.flagsBuf.destroy(); state.flagsBuf = null; } if (state.pickColorsBuf) { state.pickColorsBuf.destroy(); state.pickColorsBuf = null; } if (state.indicesBuf) { state.indicesBuf.destroy(); state.indicessBuf = null; } if (state.edgeIndicesBuf) { state.edgeIndicesBuf.destroy(); state.edgeIndicessBuf = null; } state.destroy(); } } /** * @private */ class TrianglesInstancingRenderer extends VBORenderer { constructor(scene, withSAO, {edges = false, useAlphaCutoff = false} = {}) { super(scene, withSAO, {instancing: true, edges, useAlphaCutoff}); } _draw(drawCfg) { const {gl} = this._scene.canvas; const { state, frameCtx, incrementDrawState } = drawCfg; if (this._edges) { if (state.edgeIndicesBuf) { gl.drawElementsInstanced(gl.LINES, state.edgeIndicesBuf.numItems, state.edgeIndicesBuf.itemType, 0, state.numInstances); } } else { gl.drawElementsInstanced(gl.TRIANGLES, state.indicesBuf.numItems, state.indicesBuf.itemType, 0, state.numInstances); if (incrementDrawState) { frameCtx.drawElements++; } } } } /** * @private */ class TrianglesColorRenderer extends TrianglesInstancingRenderer { _getHash() { const scene = this._scene; return [scene._lightsState.getHash(), scene._sectionPlanesState.getHash(), (this._withSAO ? "sao" : "nosao")].join(";"); } drawLayer(frameCtx, layer, renderPass) { super.drawLayer(frameCtx, layer, renderPass, { incrementDrawState: true }); } _buildVertexShader() { const scene = this._scene; const sectionPlanesState = scene._sectionPlanesState; const lightsState = scene._lightsState; const clipping = sectionPlanesState.getNumAllocatedSectionPlanes() > 0; let i; let len; let light; const src = []; src.push("#version 300 es"); src.push("// Instancing geometry drawing vertex shader"); src.push("uniform int renderPass;"); src.push("in vec3 position;"); src.push("in vec2 normal;"); src.push("in vec4 color;"); src.push("in float flags;"); if (scene.entityOffsetsEnabled) { src.push("in vec3 offset;"); } src.push("in vec4 modelMatrixCol0;"); // Modeling matrix src.push("in vec4 modelMatrixCol1;"); src.push("in vec4 modelMatrixCol2;"); src.push("in vec4 modelNormalMatrixCol0;"); src.push("in vec4 modelNormalMatrixCol1;"); src.push("in vec4 modelNormalMatrixCol2;"); this._addMatricesUniformBlockLines(src, true); if (scene.logarithmicDepthBufferEnabled) { src.push("uniform float logDepthBufFC;"); src.push("out float vFragDepth;"); src.push("bool isPerspectiveMatrix(mat4 m) {"); src.push(" return (m[2][3] == - 1.0);"); src.push("}"); src.push("out float isPerspective;"); } src.push("uniform vec4 lightAmbient;"); for (i = 0, len = lightsState.lights.length; i < len; i++) { light = lightsState.lights[i]; if (light.type === "ambient") { continue; } src.push("uniform vec4 lightColor" + i + ";"); if (light.type === "dir") { src.push("uniform vec3 lightDir" + i + ";"); } if (light.type === "point") { src.push("uniform vec3 lightPos" + i + ";"); } if (light.type === "spot") { src.push("uniform vec3 lightPos" + i + ";"); src.push("uniform vec3 lightDir" + i + ";"); } } src.push("vec3 octDecode(vec2 oct) {"); src.push(" vec3 v = vec3(oct.xy, 1.0 - abs(oct.x) - abs(oct.y));"); src.push(" if (v.z < 0.0) {"); src.push(" v.xy = (1.0 - abs(v.yx)) * vec2(v.x >= 0.0 ? 1.0 : -1.0, v.y >= 0.0 ? 1.0 : -1.0);"); src.push(" }"); src.push(" return normalize(v);"); src.push("}"); if (clipping) { src.push("out vec4 vWorldPosition;"); src.push("out float vFlags;"); } src.push("out vec4 vColor;"); src.push("void main(void) {"); // colorFlag = NOT_RENDERED | COLOR_OPAQUE | COLOR_TRANSPARENT // renderPass = COLOR_OPAQUE | COLOR_TRANSPARENT src.push(`int colorFlag = int(flags) & 0xF;`); src.push(`if (colorFlag != renderPass) {`); src.push(" gl_Position = vec4(0.0, 0.0, 0.0, 0.0);"); // Cull vertex src.push("} else {"); src.push("vec4 worldPosition = positionsDecodeMatrix * vec4(position, 1.0); "); src.push("worldPosition = worldMatrix * vec4(dot(worldPosition, modelMatrixCol0), dot(worldPosition, modelMatrixCol1), dot(worldPosition, modelMatrixCol2), 1.0);"); if (scene.entityOffsetsEnabled) { src.push("worldPosition.xyz = worldPosition.xyz + offset;"); } src.push("vec4 viewPosition = viewMatrix * worldPosition; "); src.push("vec4 modelNormal = vec4(octDecode(normal.xy), 0.0); "); src.push("vec4 worldNormal = worldNormalMatrix * vec4(dot(modelNormal, modelNormalMatrixCol0), dot(modelNormal, modelNormalMatrixCol1), dot(modelNormal, modelNormalMatrixCol2), 0.0);"); src.push("vec3 viewNormal = normalize(vec4(viewNormalMatrix * worldNormal).xyz);"); src.push("vec3 reflectedColor = vec3(0.0, 0.0, 0.0);"); src.push("vec3 viewLightDir = vec3(0.0, 0.0, -1.0);"); src.push("float lambertian = 1.0;"); for (i = 0, len = lightsState.lights.length; i < len; i++) { light = lightsState.lights[i]; if (light.type === "ambient") { continue; } if (light.type === "dir") { if (light.space === "view") { src.push("viewLightDir = normalize(lightDir" + i + ");"); } else { src.push("viewLightDir = normalize((viewMatrix * vec4(lightDir" + i + ", 0.0)).xyz);"); } } else if (light.type === "point") { if (light.space === "view") { src.push("viewLightDir = -normalize(lightPos" + i + " - viewPosition.xyz);"); } else { src.push("viewLightDir = -normalize((viewMatrix * vec4(lightPos" + i + ", 0.0)).xyz);"); } } else if (light.type === "spot") { if (light.space === "view") { src.push("viewLightDir = normalize(lightDir" + i + ");"); } else { src.push("viewLightDir = normalize((viewMatrix * vec4(lightDir" + i + ", 0.0)).xyz);"); } } else { continue; } src.push("lambertian = max(dot(-viewNormal, viewLightDir), 0.0);"); src.push("reflectedColor += lambertian * (lightColor" + i + ".rgb * lightColor" + i + ".a);"); } src.push("vec3 rgb = (vec3(float(color.r) / 255.0, float(color.g) / 255.0, float(color.b) / 255.0));"); src.push("vColor = vec4((lightAmbient.rgb * lightAmbient.a * rgb) + (reflectedColor * rgb), float(color.a) / 255.0);"); src.push("vec4 clipPos = projMatrix * viewPosition;"); if (scene.logarithmicDepthBufferEnabled) { src.push("vFragDepth = 1.0 + clipPos.w;"); src.push("isPerspective = float (isPerspectiveMatrix(projMatrix));"); } if (clipping) { src.push("vWorldPosition = worldPosition;"); src.push("vFlags = flags;"); } src.push("gl_Position = clipPos;"); src.push("}"); src.push("}"); return src; } _buildFragmentShader() { const scene = this._scene; const sectionPlanesState = scene._sectionPlanesState; const clipping = sectionPlanesState.getNumAllocatedSectionPlanes() > 0; const src = []; src.push("#version 300 es"); src.push("// Instancing geometry drawing fragment shader"); src.push("#ifdef GL_FRAGMENT_PRECISION_HIGH"); src.push("precision highp float;"); src.push("precision highp int;"); src.push("#else"); src.push("precision mediump float;"); src.push("precision mediump int;"); src.push("#endif"); if (scene.logarithmicDepthBufferEnabled) { src.push("in float isPerspective;"); src.push("uniform float logDepthBufFC;"); src.push("in float vFragDepth;"); } if (this._withSAO) { src.push("uniform sampler2D uOcclusionTexture;"); src.push("uniform vec4 uSAOParams;"); src.push("const float packUpscale = 256. / 255.;"); src.push("const float unpackDownScale = 255. / 256.;"); src.push("const vec3 packFactors = vec3( 256. * 256. * 256., 256. * 256., 256. );"); src.push("const vec4 unPackFactors = unpackDownScale / vec4( packFactors, 1. );"); src.push("float unpackRGBToFloat( const in vec4 v ) {"); src.push(" return dot( v, unPackFactors );"); src.push("}"); } if (clipping) { src.push("in vec4 vWorldPosition;"); src.push("in float vFlags;"); for (let i = 0, len = sectionPlanesState.getNumAllocatedSectionPlanes(); i < len; i++) { src.push("uniform bool sectionPlaneActive" + i + ";"); src.push("uniform vec3 sectionPlanePos" + i + ";"); src.push("uniform vec3 sectionPlaneDir" + i + ";"); } src.push("uniform float sliceThickness;"); src.push("uniform vec4 sliceColor;"); } src.push("in vec4 vColor;"); src.push("out vec4 outColor;"); src.push("void main(void) {"); src.push(" vec4 newColor;"); src.push(" newColor = vColor;"); if (clipping) { src.push(" bool clippable = (int(vFlags) >> 16 & 0xF) == 1;"); src.push(" if (clippable) {"); src.push(" float dist = 0.0;"); for (let i = 0, len = sectionPlanesState.getNumAllocatedSectionPlanes(); i < len; i++) { src.push("if (sectionPlaneActive" + i + ") {"); src.push(" dist += clamp(dot(-sectionPlaneDir" + i + ".xyz, vWorldPosition.xyz - sectionPlanePos" + i + ".xyz), 0.0, 1000.0);"); src.push("}"); } src.push(" if (dist > sliceThickness) { "); src.push(" discard;"); src.push(" }"); src.push(" if (dist > 0.0) { "); src.push(" newColor = sliceColor;"); src.push(" }"); src.push("}"); } if (scene.logarithmicDepthBufferEnabled) { src.push(" float dx = dFdx(vFragDepth);"); src.push(" float dy = dFdy(vFragDepth);"); src.push(" float diff = sqrt(dx*dx+dy*dy);"); src.push(" gl_FragDepth = isPerspective == 0.0 ? gl_FragCoord.z : log2( vFragDepth + diff ) * logDepthBufFC * 0.5;"); } else { src.push(" float dx = dFdx(gl_FragCoord.z);"); src.push(" float dy = dFdy(gl_FragCoord.z);"); src.push(" float diff = sqrt(dx*dx+dy*dy);"); src.push(" gl_FragDepth = gl_FragCoord.z + diff;"); } // Doing SAO blend in the main solid fill draw shader just so that edge lines can be drawn over the top // Would be more efficient to defer this, then render lines later, using same depth buffer for Z-reject if (this._withSAO) { src.push(" float viewportWidth = uSAOParams[0];"); src.push(" float viewportHeight = uSAOParams[1];"); src.push(" float blendCutoff = uSAOParams[2];"); src.push(" float blendFactor = uSAOParams[3];"); src.push(" vec2 uv = vec2(gl_FragCoord.x / viewportWidth, gl_FragCoord.y / viewportHeight);"); src.push(" float ambient = smoothstep(blendCutoff, 1.0, unpackRGBToFloat(texture(uOcclusionTexture, uv))) * blendFactor;"); src.push(" outColor = vec4(newColor.rgb * ambient, 1.0);"); } else { src.push(" outColor = newColor;"); } src.push("}"); return src; } } /** * @private */ class TrianglesFlatColorRenderer extends TrianglesInstancingRenderer { _getHash() { const scene = this._scene; return [scene._lightsState.getHash(), scene._sectionPlanesState.getHash(), (this._withSAO ? "sao" : "nosao")].join(";"); } _buildVertexShader() { const scene = this._scene; const sectionPlanesState = scene._sectionPlanesState; const clipping = sectionPlanesState.getNumAllocatedSectionPlanes() > 0; const src = []; src.push("#version 300 es"); src.push("// Instancing geometry flat-shading drawing vertex shader"); src.push("uniform int renderPass;"); src.push("in vec3 position;"); src.push("in vec4 color;"); src.push("in float flags;"); if (scene.entityOffsetsEnabled) { src.push("in vec3 offset;"); } src.push("in vec4 modelMatrixCol0;"); // Modeling matrix src.push("in vec4 modelMatrixCol1;"); src.push("in vec4 modelMatrixCol2;"); this._addMatricesUniformBlockLines(src); if (scene.logarithmicDepthBufferEnabled) { src.push("uniform float logDepthBufFC;"); src.push("out float vFragDepth;"); src.push("bool isPerspectiveMatrix(mat4 m) {"); src.push(" return (m[2][3] == - 1.0);"); src.push("}"); src.push("out float isPerspective;"); } if (clipping) { src.push("out vec4 vWorldPosition;"); src.push("out float vFlags;"); } src.push("out vec4 vViewPosition;"); src.push("out vec4 vColor;"); src.push("void main(void) {"); // colorFlag = NOT_RENDERED | COLOR_OPAQUE | COLOR_TRANSPARENT // renderPass = COLOR_OPAQUE | COLOR_TRANSPARENT src.push(`int colorFlag = int(flags) & 0xF;`); src.push(`if (colorFlag != renderPass) {`); src.push(" gl_Position = vec4(0.0, 0.0, 0.0, 0.0);"); // Cull vertex src.push("} else {"); src.push("vec4 worldPosition = positionsDecodeMatrix * vec4(position, 1.0); "); src.push("worldPosition = worldMatrix * vec4(dot(worldPosition, modelMatrixCol0), dot(worldPosition, modelMatrixCol1), dot(worldPosition, modelMatrixCol2), 1.0);"); if (scene.entityOffsetsEnabled) { src.push(" worldPosition.xyz = worldPosition.xyz + offset;"); } src.push("vec4 viewPosition = viewMatrix * worldPosition; "); src.push("vViewPosition = viewPosition;"); src.push("vColor = vec4(float(color.r) / 255.0, float(color.g) / 255.0, float(color.b) / 255.0, float(color.a) / 255.0);"); src.push("vec4 clipPos = projMatrix * viewPosition;"); if (scene.logarithmicDepthBufferEnabled) { src.push("vFragDepth = 1.0 + clipPos.w;"); src.push("isPerspective = float (isPerspectiveMatrix(projMatrix));"); } if (clipping) { src.push("vWorldPosition = worldPosition;"); src.push("vFlags = flags;"); } src.push("gl_Position = clipPos;"); src.push("}"); src.push("}"); return src; } _buildFragmentShader() { const scene = this._scene; const sectionPlanesState = scene._sectionPlanesState; const lightsState = scene._lightsState; let i; let len; const clipping = sectionPlanesState.getNumAllocatedSectionPlanes() > 0; const src = []; src.push("#version 300 es"); src.push("// Instancing geometry flat-shading drawing fragment shader"); src.push("#ifdef GL_FRAGMENT_PRECISION_HIGH"); src.push("precision highp float;"); src.push("precision highp int;"); src.push("#else"); src.push("precision mediump float;"); src.push("precision mediump int;"); src.push("#endif"); if (scene.logarithmicDepthBufferEnabled) { src.push("in float isPerspective;"); src.push("uniform float logDepthBufFC;"); src.push("in float vFragDepth;"); } if (this._withSAO) { src.push("uniform sampler2D uOcclusionTexture;"); src.push("uniform vec4 uSAOParams;"); src.push("const float packUpscale = 256. / 255.;"); src.push("const float unpackDownScale = 255. / 256.;"); src.push("const vec3 packFactors = vec3( 256. * 256. * 256., 256. * 256., 256. );"); src.push("const vec4 unPackFactors = unpackDownScale / vec4( packFactors, 1. );"); src.push("float unpackRGBToFloat( const in vec4 v ) {"); src.push(" return dot( v, unPackFactors );"); src.push("}"); } if (clipping) { src.push("in vec4 vWorldPosition;"); src.push("in float vFlags;"); for (let i = 0, len = sectionPlanesState.getNumAllocatedSectionPlanes(); i < len; i++) { src.push("uniform bool sectionPlaneActive" + i + ";"); src.push("uniform vec3 sectionPlanePos" + i + ";"); src.push("uniform vec3 sectionPlaneDir" + i + ";"); } src.push("uniform float sliceThickness;"); src.push("uniform vec4 sliceColor;"); } this._addMatricesUniformBlockLines(src); src.push("uniform vec4 lightAmbient;"); for (i = 0, len = lightsState.lights.length; i < len; i++) { const light = lightsState.lights[i]; if (light.type === "ambient") { continue; } src.push("uniform vec4 lightColor" + i + ";"); if (light.type === "dir") { src.push("uniform vec3 lightDir" + i + ";"); } if (light.type === "point") { src.push("uniform vec3 lightPos" + i + ";"); } if (light.type === "spot") { src.push("uniform vec3 lightPos" + i + ";"); src.push("uniform vec3 lightDir" + i + ";"); } } src.push("in vec4 vViewPosition;"); src.push("in vec4 vColor;"); src.push("out vec4 outColor;"); src.push("void main(void) {"); src.push(" vec4 newColor;"); src.push(" newColor = vColor;"); if (clipping) { src.push(" bool clippable = (int(vFlags) >> 16 & 0xF) == 1;"); src.push(" if (clippable) {"); src.push(" float dist = 0.0;"); for (let i = 0, len = sectionPlanesState.getNumAllocatedSectionPlanes(); i < len; i++) { src.push("if (sectionPlaneActive" + i + ") {"); src.push(" dist += clamp(dot(-sectionPlaneDir" + i + ".xyz, vWorldPosition.xyz - sectionPlanePos" + i + ".xyz), 0.0, 1000.0);"); src.push("}"); } src.push(" if (dist > sliceThickness) { "); src.push(" discard;"); src.push(" }"); src.push(" if (dist > 0.0) { "); src.push(" newColor = sliceColor;"); src.push(" }"); src.push("}"); } src.push("vec3 reflectedColor = vec3(0.0, 0.0, 0.0);"); src.push("vec3 viewLightDir = vec3(0.0, 0.0, -1.0);"); src.push("float lambertian = 1.0;"); src.push("vec3 xTangent = dFdx( vViewPosition.xyz );"); src.push("vec3 yTangent = dFdy( vViewPosition.xyz );"); src.push("vec3 viewNormal = normalize( cross( xTangent, yTangent ) );"); for (i = 0, len = lightsState.lights.length; i < len; i++) { const light = lightsState.lights[i]; if (light.type === "ambient") { continue; } if (light.type === "dir") { if (light.space === "view") { src.push("viewLightDir = normalize(lightDir" + i + ");"); } else { src.push("viewLightDir = normalize((viewMatrix * vec4(lightDir" + i + ", 0.0)).xyz);"); } } else if (light.type === "point") { if (light.space === "view") { src.push("viewLightDir = -normalize(lightPos" + i + " - viewPosition.xyz);"); } else { src.push("viewLightDir = -normalize((viewMatrix * vec4(lightPos" + i + ", 0.0)).xyz);"); } } else if (light.type === "spot") { if (light.space === "view") { src.push("viewLightDir = normalize(lightDir" + i + ");"); } else { src.push("viewLightDir = normalize((viewMatrix * vec4(lightDir" + i + ", 0.0)).xyz);"); } } else { continue; } src.push("lambertian = max(dot(-viewNormal, viewLightDir), 0.0);"); src.push("reflectedColor += lambertian * (lightColor" + i + ".rgb * lightColor" + i + ".a);"); } src.push("vec4 fragColor = vec4((lightAmbient.rgb * lightAmbient.a * newColor.rgb) + (reflectedColor * newColor.rgb), newColor.a);"); if (this._withSAO) { // Doing SAO blend in the main solid fill draw shader just so that edge lines can be drawn over the top // Would be more efficient to defer this, then render lines later, using same depth buffer for Z-reject src.push(" float viewportWidth = uSAOParams[0];"); src.push(" float viewportHeight = uSAOParams[1];"); src.push(" float blendCutoff = uSAOParams[2];"); src.push(" float blendFactor = uSAOParams[3];"); src.push(" vec2 uv = vec2(gl_FragCoord.x / viewportWidth, gl_FragCoord.y / viewportHeight);"); src.push(" float ambient = smoothstep(blendCutoff, 1.0, unpackRGBToFloat(texture(uOcclusionTexture, uv))) * blendFactor;"); src.push(" outColor = vec4(fragColor.rgb * ambient, 1.0);"); } else { src.push(" outColor = fragColor;"); } if (scene.logarithmicDepthBufferEnabled) { src.push(" gl_FragDepth = isPerspective == 0.0 ? gl_FragCoord.z : log2( vFragDepth ) * logDepthBufFC * 0.5;"); } else { src.push(" float dx = dFdx(gl_FragCoord.z);"); src.push(" float dy = dFdy(gl_FragCoord.z);"); src.push(" float diff = sqrt(dx*dx+dy*dy);"); src.push(" gl_FragDepth = gl_FragCoord.z + diff;"); } src.push("}"); return src; } } /** * @private */ class TrianglesSilhouetteRenderer extends TrianglesInstancingRenderer { drawLayer(frameCtx, instancingLayer, renderPass) { // TODO color uniform true ??? super.drawLayer(frameCtx, instancingLayer, renderPass, { colorUniform: true }); } _buildVertexShader() { const scene = this._scene; const sectionPlanesState = scene._sectionPlanesState; const clipping = sectionPlanesState.getNumAllocatedSectionPlanes() > 0; const src = []; src.push("#version 300 es"); src.push("// Instancing silhouette vertex shader"); src.push("uniform int renderPass;"); src.push("in vec3 position;"); if (scene.entityOffsetsEnabled) { src.push("in vec3 offset;"); } src.push("in float flags;"); src.push("in vec4 color;"); src.push("in vec4 modelMatrixCol0;"); // Modeling matrix src.push("in vec4 modelMatrixCol1;"); src.push("in vec4 modelMatrixCol2;"); this._addMatricesUniformBlockLines(src); src.push("uniform vec4 silhouetteColor;"); if (scene.logarithmicDepthBufferEnabled) { src.push("uniform float logDepthBufFC;"); src.push("out float vFragDepth;"); src.push("bool isPerspectiveMatrix(mat4 m) {"); src.push(" return (m[2][3] == - 1.0);"); src.push("}"); src.push("out float isPerspective;"); } if (clipping) { src.push("out vec4 vWorldPosition;"); src.push("out float vFlags;"); } src.push("out vec4 vColor;"); src.push("void main(void) {"); // silhouetteFlag = NOT_RENDERED | SILHOUETTE_HIGHLIGHTED | SILHOUETTE_SELECTED | | SILHOUETTE_XRAYED // renderPass = SILHOUETTE_HIGHLIGHTED | SILHOUETTE_SELECTED | | SILHOUETTE_XRAYED src.push(`int silhouetteFlag = int(flags) >> 4 & 0xF;`); src.push(`if (silhouetteFlag != renderPass) {`); src.push(" gl_Position = vec4(0.0, 0.0, 0.0, 0.0);"); // Cull vertex src.push("} else {"); src.push("vec4 worldPosition = positionsDecodeMatrix * vec4(position, 1.0); "); src.push("worldPosition = worldMatrix * vec4(dot(worldPosition, modelMatrixCol0), dot(worldPosition, modelMatrixCol1), dot(worldPosition, modelMatrixCol2), 1.0);"); if (scene.entityOffsetsEnabled) { src.push(" worldPosition.xyz = worldPosition.xyz + offset;"); } src.push("vec4 viewPosition = viewMatrix * worldPosition; "); if (clipping) { src.push("vWorldPosition = worldPosition;"); src.push("vFlags = flags;"); } src.push("vColor = vec4(silhouetteColor.r, silhouetteColor.g, silhouetteColor.b, min(silhouetteColor.a, float(color.a) / 255.0));"); src.push("vec4 clipPos = projMatrix * viewPosition;"); if (scene.logarithmicDepthBufferEnabled) { src.push("vFragDepth = 1.0 + clipPos.w;"); src.push("isPerspective = float (isPerspectiveMatrix(projMatrix));"); } src.push("gl_Position = clipPos;"); src.push("}"); src.push("}"); return src; } _buildFragmentShader() { const scene = this._scene; const sectionPlanesState = scene._sectionPlanesState; const clipping = sectionPlanesState.getNumAllocatedSectionPlanes() > 0; const src = []; src.push("#version 300 es"); src.push("// Instancing fill fragment shader"); src.push("#ifdef GL_FRAGMENT_PRECISION_HIGH"); src.push("precision highp float;"); src.push("precision highp int;"); src.push("#else"); src.push("precision mediump float;"); src.push("precision mediump int;"); src.push("#endif"); if (scene.logarithmicDepthBufferEnabled) { src.push("in float isPerspective;"); src.push("uniform float logDepthBufFC;"); src.push("in float vFragDepth;"); } if (clipping) { src.push("in vec4 vWorldPosition;"); src.push("in float vFlags;"); for (let i = 0, len = sectionPlanesState.getNumAllocatedSectionPlanes(); i < len; i++) { src.push("uniform bool sectionPlaneActive" + i + ";"); src.push("uniform vec3 sectionPlanePos" + i + ";"); src.push("uniform vec3 sectionPlaneDir" + i + ";"); } src.push("uniform float sliceThickness;"); src.push("uniform vec4 sliceColor;"); } src.push("in vec4 vColor;"); src.push("out vec4 outColor;"); src.push("void main(void) {"); src.push(" vec4 newColor;"); src.push(" newColor = vColor;"); if (clipping) { src.push(" bool clippable = (int(vFlags) >> 16 & 0xF) == 1;"); src.push(" if (clippable) {"); src.push(" float dist = 0.0;"); for (let i = 0, len = sectionPlanesState.getNumAllocatedSectionPlanes(); i < len; i++) { src.push("if (sectionPlaneActive" + i + ") {"); src.push(" dist += clamp(dot(-sectionPlaneDir" + i + ".xyz, vWorldPosition.xyz - sectionPlanePos" + i + ".xyz), 0.0, 1000.0);"); src.push("}"); } src.push(" if (dist > sliceThickness) { "); src.push(" discard;"); src.push(" }"); src.push(" if (dist > 0.0) { "); src.push(" newColor = sliceColor;"); src.push(" }"); src.push("}"); } if (scene.logarithmicDepthBufferEnabled) { src.push(" gl_FragDepth = isPerspective == 0.0 ? gl_FragCoord.z : log2( vFragDepth ) * logDepthBufFC * 0.5;"); } src.push("outColor = newColor;"); src.push("}"); return src; } } /** * @private */ class EdgesRenderer extends TrianglesInstancingRenderer { constructor(scene, withSAO) { super(scene, withSAO, {instancing: true, edges: true}); } } /** * @private */ class EdgesEmphasisRenderer extends EdgesRenderer { drawLayer(frameCtx, instancingLayer, renderPass) { super.drawLayer(frameCtx, instancingLayer, renderPass, {colorUniform: true}); } _buildVertexShader() { const scene = this._scene; const sectionPlanesState = scene._sectionPlanesState; const clipping = sectionPlanesState.getNumAllocatedSectionPlanes() > 0; const src = []; src.push("#version 300 es"); src.push("// EdgesEmphasisRenderer vertex shader"); src.push("uniform int renderPass;"); src.push("uniform vec4 color;"); src.push("in vec3 position;"); if (scene.entityOffsetsEnabled) { src.push("in vec3 offset;"); } src.push("in float flags;"); src.push("in vec4 modelMatrixCol0;"); // Modeling matrix src.push("in vec4 modelMatrixCol1;"); src.push("in vec4 modelMatrixCol2;"); this._addMatricesUniformBlockLines(src); if (scene.logarithmicDepthBufferEnabled) { src.push("uniform float logDepthBufFC;"); src.push("out float vFragDepth;"); src.push("bool isPerspectiveMatrix(mat4 m) {"); src.push(" return (m[2][3] == - 1.0);"); src.push("}"); src.push("out float isPerspective;"); } if (clipping) { src.push("out vec4 vWorldPosition;"); src.push("out float vFlags;"); } src.push("out vec4 vColor;"); src.push("void main(void) {"); // edgeFlag = NOT_RENDERED | EDGES_COLOR_OPAQUE | EDGES_HIGHLIGHTED | EDGES_XRAYED | EDGES_SELECTED // renderPass = EDGES_HIGHLIGHTED | EDGES_XRAYED | EDGES_SELECTED src.push(`int edgeFlag = int(flags) >> 8 & 0xF;`); src.push(`if (edgeFlag != renderPass) {`); src.push(" gl_Position = vec4(0.0, 0.0, 0.0, 0.0);"); // Cull vertex src.push("} else {"); src.push("vec4 worldPosition = worldMatrix * positionsDecodeMatrix * vec4(position, 1.0); "); src.push("worldPosition = worldMatrix * vec4(dot(worldPosition, modelMatrixCol0), dot(worldPosition, modelMatrixCol1), dot(worldPosition, modelMatrixCol2), 1.0);"); if (scene.entityOffsetsEnabled) { src.push(" worldPosition.xyz = worldPosition.xyz + offset;"); } src.push("vec4 viewPosition = viewMatrix * worldPosition; "); if (clipping) { src.push("vWorldPosition = worldPosition;"); src.push("vFlags = flags;"); } src.push("vec4 clipPos = projMatrix * viewPosition;"); if (scene.logarithmicDepthBufferEnabled) { src.push("vFragDepth = 1.0 + clipPos.w;"); src.push("isPerspective = float (isPerspectiveMatrix(projMatrix));"); } src.push("gl_Position = clipPos;"); src.push("vColor = vec4(color.r, color.g, color.b, color.a);"); src.push("}"); src.push("}"); return src; } _buildFragmentShader() { const scene = this._scene; const sectionPlanesState = scene._sectionPlanesState; const clipping = sectionPlanesState.getNumAllocatedSectionPlanes() > 0; const src = []; src.push("#version 300 es"); src.push("// EdgesEmphasisRenderer fragment shader"); src.push("#ifdef GL_FRAGMENT_PRECISION_HIGH"); src.push("precision highp float;"); src.push("precision highp int;"); src.push("#else"); src.push("precision mediump float;"); src.push("precision mediump int;"); src.push("#endif"); if (scene.logarithmicDepthBufferEnabled) { src.push("in float isPerspective;"); src.push("uniform float logDepthBufFC;"); src.push("in float vFragDepth;"); } if (clipping) { src.push("in vec4 vWorldPosition;"); src.push("in float vFlags;"); for (let i = 0, len = sectionPlanesState.getNumAllocatedSectionPlanes(); i < len; i++) { src.push("uniform bool sectionPlaneActive" + i + ";"); src.push("uniform vec3 sectionPlanePos" + i + ";"); src.push("uniform vec3 sectionPlaneDir" + i + ";"); } } src.push("in vec4 vColor;"); src.push("out vec4 outColor;"); src.push("void main(void) {"); if (clipping) { src.push(" bool clippable = (int(vFlags) >> 16 & 0xF) == 1;"); src.push(" if (clippable) {"); src.push(" float dist = 0.0;"); for (let i = 0, len = sectionPlanesState.getNumAllocatedSectionPlanes(); i < len; i++) { src.push("if (sectionPlaneActive" + i + ") {"); src.push(" dist += clamp(dot(-sectionPlaneDir" + i + ".xyz, vWorldPosition.xyz - sectionPlanePos" + i + ".xyz), 0.0, 1000.0);"); src.push("}"); } src.push(" if (dist > 0.0) { discard; }"); src.push("}"); } if (scene.logarithmicDepthBufferEnabled) { src.push(" gl_FragDepth = isPerspective == 0.0 ? gl_FragCoord.z : log2( vFragDepth ) * logDepthBufFC * 0.5;"); } src.push("outColor = vColor;"); src.push("}"); return src; } } /** * @private */ class EdgesColorRenderer extends EdgesRenderer { drawLayer(frameCtx, batchingLayer, renderPass) { super.drawLayer(frameCtx, batchingLayer, renderPass, { colorUniform: false }); } _buildVertexShader() { const scene = this._scene; const sectionPlanesState = scene._sectionPlanesState; const clipping = sectionPlanesState.getNumAllocatedSectionPlanes() > 0; const src = []; src.push("#version 300 es"); src.push("// EdgesColorRenderer vertex shader"); src.push("uniform int renderPass;"); src.push("in vec3 position;"); src.push("in vec4 color;"); if (scene.entityOffsetsEnabled) { src.push("in vec3 offset;"); } src.push("in float flags;"); src.push("in vec4 modelMatrixCol0;"); // Modeling matrix src.push("in vec4 modelMatrixCol1;"); src.push("in vec4 modelMatrixCol2;"); this._addMatricesUniformBlockLines(src); if (scene.logarithmicDepthBufferEnabled) { src.push("uniform float logDepthBufFC;"); src.push("out float vFragDepth;"); src.push("bool isPerspectiveMatrix(mat4 m) {"); src.push(" return (m[2][3] == - 1.0);"); src.push("}"); src.push("out float isPerspective;"); } if (clipping) { src.push("out vec4 vWorldPosition;"); src.push("out float vFlags;"); } src.push("out vec4 vColor;"); src.push("void main(void) {"); // edgeFlag = NOT_RENDERED | EDGES_COLOR_OPAQUE | EDGES_HIGHLIGHTED | EDGES_XRAYED | EDGES_SELECTED // renderPass = EDGES_HIGHLIGHTED | EDGES_XRAYED | EDGES_SELECTED src.push(`int edgeFlag = int(flags) >> 8 & 0xF;`); src.push(`if (edgeFlag != renderPass) {`); src.push(" gl_Position = vec4(0.0, 0.0, 0.0, 0.0);"); // Cull vertex src.push("} else {"); src.push("vec4 worldPosition = positionsDecodeMatrix * vec4(position, 1.0); "); src.push("worldPosition = worldMatrix * vec4(dot(worldPosition, modelMatrixCol0), dot(worldPosition, modelMatrixCol1), dot(worldPosition, modelMatrixCol2), 1.0);"); if (scene.entityOffsetsEnabled) { src.push(" worldPosition.xyz = worldPosition.xyz + offset;"); } src.push("vec4 viewPosition = viewMatrix * worldPosition; "); if (clipping) { src.push("vWorldPosition = worldPosition;"); src.push("vFlags = flags;"); } src.push("vec4 clipPos = projMatrix * viewPosition;"); if (scene.logarithmicDepthBufferEnabled) { src.push("vFragDepth = 1.0 + clipPos.w;"); src.push("isPerspective = float (isPerspectiveMatrix(projMatrix));"); } src.push("gl_Position = clipPos;"); // src.push("vColor = vec4(float(color.r-100.0) / 255.0, float(color.g-100.0) / 255.0, float(color.b-100.0) / 255.0, float(color.a) / 255.0);"); src.push("vColor = vec4(float(color.r*0.5) / 255.0, float(color.g*0.5) / 255.0, float(color.b*0.5) / 255.0, float(color.a) / 255.0);"); src.push("}"); src.push("}"); return src; } _buildFragmentShader() { const scene = this._scene; const sectionPlanesState = scene._sectionPlanesState; const clipping = sectionPlanesState.getNumAllocatedSectionPlanes() > 0; const src = []; src.push("#version 300 es"); src.push("// EdgesColorRenderer fragment shader"); src.push("#ifdef GL_FRAGMENT_PRECISION_HIGH"); src.push("precision highp float;"); src.push("precision highp int;"); src.push("#else"); src.push("precision mediump float;"); src.push("precision mediump int;"); src.push("#endif"); if (scene.logarithmicDepthBufferEnabled) { src.push("in float isPerspective;"); src.push("uniform float logDepthBufFC;"); src.push("in float vFragDepth;"); } if (clipping) { src.push("in vec4 vWorldPosition;"); src.push("in float vFlags;"); for (let i = 0, len = sectionPlanesState.getNumAllocatedSectionPlanes(); i < len; i++) { src.push("uniform bool sectionPlaneActive" + i + ";"); src.push("uniform vec3 sectionPlanePos" + i + ";"); src.push("uniform vec3 sectionPlaneDir" + i + ";"); } } src.push("in vec4 vColor;"); src.push("out vec4 outColor;"); src.push("void main(void) {"); if (clipping) { src.push(" bool clippable = (int(vFlags) >> 16 & 0xF) == 1;"); src.push(" if (clippable) {"); src.push(" float dist = 0.0;"); for (let i = 0, len = sectionPlanesState.getNumAllocatedSectionPlanes(); i < len; i++) { src.push("if (sectionPlaneActive" + i + ") {"); src.push(" dist += clamp(dot(-sectionPlaneDir" + i + ".xyz, vWorldPosition.xyz - sectionPlanePos" + i + ".xyz), 0.0, 1000.0);"); src.push("}"); } src.push(" if (dist > 0.0) { discard; }"); src.push("}"); } if (scene.logarithmicDepthBufferEnabled) { src.push(" gl_FragDepth = isPerspective == 0.0 ? gl_FragCoord.z : log2( vFragDepth ) * logDepthBufFC * 0.5;"); } src.push("outColor = vColor;"); src.push("}"); return src; } } /** * @private */ class TrianglesPickMeshRenderer extends TrianglesInstancingRenderer { _buildVertexShader() { const scene = this._scene; const sectionPlanesState = scene._sectionPlanesState; const clipping = sectionPlanesState.getNumAllocatedSectionPlanes() > 0; const src = []; src.push("#version 300 es"); src.push("// Instancing geometry picking vertex shader"); src.push("uniform int renderPass;"); src.push("in vec3 position;"); if (scene.entityOffsetsEnabled) { src.push("in vec3 offset;"); } src.push("in float flags;"); src.push("in vec4 pickColor;"); src.push("in vec4 modelMatrixCol0;"); // Modeling matrix src.push("in vec4 modelMatrixCol1;"); src.push("in vec4 modelMatrixCol2;"); src.push("uniform bool pickInvisible;"); this._addMatricesUniformBlockLines(src); this._addRemapClipPosLines(src); if (scene.logarithmicDepthBufferEnabled) { src.push("uniform float logDepthBufFC;"); src.push("out float vFragDepth;"); src.push("bool isPerspectiveMatrix(mat4 m) {"); src.push(" return (m[2][3] == - 1.0);"); src.push("}"); src.push("out float isPerspective;"); } if (clipping) { src.push("out vec4 vWorldPosition;"); src.push("out float vFlags;"); } src.push("out vec4 vPickColor;"); src.push("void main(void) {"); // pickFlag = NOT_RENDERED | PICK // renderPass = PICK src.push(`int pickFlag = int(flags) >> 12 & 0xF;`); src.push(`if (pickFlag != renderPass) {`); src.push(" gl_Position = vec4(0.0, 0.0, 0.0, 0.0);"); // Cull vertex src.push("} else {"); src.push(" vec4 worldPosition = positionsDecodeMatrix * vec4(position, 1.0); "); src.push(" worldPosition = worldMatrix * vec4(dot(worldPosition, modelMatrixCol0), dot(worldPosition, modelMatrixCol1), dot(worldPosition, modelMatrixCol2), 1.0);"); if (scene.entityOffsetsEnabled) { src.push(" worldPosition.xyz = worldPosition.xyz + offset;"); } src.push(" vec4 viewPosition = viewMatrix * worldPosition; "); src.push(" vPickColor = vec4(float(pickColor.r) / 255.0, float(pickColor.g) / 255.0, float(pickColor.b) / 255.0, float(pickColor.a) / 255.0);"); if (clipping) { src.push(" vWorldPosition = worldPosition;"); src.push(" vFlags = flags;"); } src.push("vec4 clipPos = projMatrix * viewPosition;"); if (scene.logarithmicDepthBufferEnabled) { src.push("vFragDepth = 1.0 + clipPos.w;"); src.push("isPerspective = float (isPerspectiveMatrix(projMatrix));"); } src.push("gl_Position = remapClipPos(clipPos);"); src.push("}"); src.push("}"); return src; } _buildFragmentShader() { const scene = this._scene; const sectionPlanesState = scene._sectionPlanesState; const clipping = sectionPlanesState.getNumAllocatedSectionPlanes() > 0; const src = []; src.push("#version 300 es"); src.push("// Batched geometry picking fragment shader"); src.push("#ifdef GL_FRAGMENT_PRECISION_HIGH"); src.push("precision highp float;"); src.push("precision highp int;"); src.push("#else"); src.push("precision mediump float;"); src.push("precision mediump int;"); src.push("#endif"); if (scene.logarithmicDepthBufferEnabled) { src.push("in float isPerspective;"); src.push("uniform float logDepthBufFC;"); src.push("in float vFragDepth;"); } if (clipping) { src.push("in vec4 vWorldPosition;"); src.push("in float vFlags;"); for (let i = 0; i < sectionPlanesState.getNumAllocatedSectionPlanes(); i++) { src.push("uniform bool sectionPlaneActive" + i + ";"); src.push("uniform vec3 sectionPlanePos" + i + ";"); src.push("uniform vec3 sectionPlaneDir" + i + ";"); } } src.push("in vec4 vPickColor;"); src.push("out vec4 outColor;"); src.push("void main(void) {"); if (clipping) { src.push(" bool clippable = (int(vFlags) >> 16 & 0xF) == 1;"); src.push(" if (clippable) {"); src.push(" float dist = 0.0;"); for (let i = 0; i < sectionPlanesState.getNumAllocatedSectionPlanes(); i++) { src.push("if (sectionPlaneActive" + i + ") {"); src.push(" dist += clamp(dot(-sectionPlaneDir" + i + ".xyz, vWorldPosition.xyz - sectionPlanePos" + i + ".xyz), 0.0, 1000.0);"); src.push("}"); } src.push("if (dist > 0.0) { discard; }"); src.push("}"); } if (scene.logarithmicDepthBufferEnabled) { src.push(" gl_FragDepth = isPerspective == 0.0 ? gl_FragCoord.z : log2( vFragDepth ) * logDepthBufFC * 0.5;"); } src.push("outColor = vPickColor; "); src.push("}"); return src; } } /** * @private */ class TrianglesPickDepthRenderer extends TrianglesInstancingRenderer { _buildVertexShader() { const scene = this._scene; const sectionPlanesState = scene._sectionPlanesState; const clipping = sectionPlanesState.getNumAllocatedSectionPlanes() > 0; const src = []; src.push("#version 300 es"); src.push("// Instancing geometry depth vertex shader"); src.push("uniform int renderPass;"); src.push("in vec3 position;"); if (scene.entityOffsetsEnabled) { src.push("in vec3 offset;"); } src.push("in float flags;"); src.push("in vec4 modelMatrixCol0;"); // Modeling matrix src.push("in vec4 modelMatrixCol1;"); src.push("in vec4 modelMatrixCol2;"); src.push("uniform bool pickInvisible;"); this._addMatricesUniformBlockLines(src); this._addRemapClipPosLines(src); if (scene.logarithmicDepthBufferEnabled) { src.push("uniform float logDepthBufFC;"); src.push("out float vFragDepth;"); src.push("bool isPerspectiveMatrix(mat4 m) {"); src.push(" return (m[2][3] == - 1.0);"); src.push("}"); src.push("out float isPerspective;"); } if (clipping) { src.push("out vec4 vWorldPosition;"); src.push("out float vFlags;"); } src.push("out vec4 vViewPosition;"); src.push("void main(void) {"); // pickFlag = NOT_RENDERED | PICK // renderPass = PICK src.push(`int pickFlag = int(flags) >> 12 & 0xF;`); src.push(`if (pickFlag != renderPass) {`); src.push(" gl_Position = vec4(0.0, 0.0, 0.0, 0.0);"); // Cull vertex src.push("} else {"); src.push(" vec4 worldPosition = positionsDecodeMatrix * vec4(position, 1.0); "); src.push(" worldPosition = worldMatrix * vec4(dot(worldPosition, modelMatrixCol0), dot(worldPosition, modelMatrixCol1), dot(worldPosition, modelMatrixCol2), 1.0);"); if (scene.entityOffsetsEnabled) { src.push(" worldPosition.xyz = worldPosition.xyz + offset;"); } src.push(" vec4 viewPosition = viewMatrix * worldPosition; "); if (clipping) { src.push(" vWorldPosition = worldPosition;"); src.push(" vFlags = flags;"); } src.push(" vViewPosition = viewPosition;"); src.push("vec4 clipPos = projMatrix * viewPosition;"); if (scene.logarithmicDepthBufferEnabled) { src.push("vFragDepth = 1.0 + clipPos.w;"); src.push("isPerspective = float (isPerspectiveMatrix(projMatrix));"); } src.push("gl_Position = remapClipPos(clipPos);"); src.push("}"); src.push("}"); return src; } _buildFragmentShader() { const scene = this._scene; const sectionPlanesState = scene._sectionPlanesState; const clipping = sectionPlanesState.getNumAllocatedSectionPlanes() > 0; const src = []; src.push("#version 300 es"); src.push("// Batched geometry depth fragment shader"); src.push("#ifdef GL_FRAGMENT_PRECISION_HIGH"); src.push("precision highp float;"); src.push("precision highp int;"); src.push("#else"); src.push("precision mediump float;"); src.push("precision mediump int;"); src.push("#endif"); if (scene.logarithmicDepthBufferEnabled) { src.push("in float isPerspective;"); src.push("uniform float logDepthBufFC;"); src.push("in float vFragDepth;"); } src.push("uniform float pickZNear;"); src.push("uniform float pickZFar;"); if (clipping) { src.push("in vec4 vWorldPosition;"); src.push("in float vFlags;"); for (let i = 0; i < sectionPlanesState.getNumAllocatedSectionPlanes(); i++) { src.push("uniform bool sectionPlaneActive" + i + ";"); src.push("uniform vec3 sectionPlanePos" + i + ";"); src.push("uniform vec3 sectionPlaneDir" + i + ";"); } } src.push("in vec4 vViewPosition;"); src.push("vec4 packDepth(const in float depth) {"); src.push(" const vec4 bitShift = vec4(256.0*256.0*256.0, 256.0*256.0, 256.0, 1.0);"); src.push(" const vec4 bitMask = vec4(0.0, 1.0/256.0, 1.0/256.0, 1.0/256.0);"); src.push(" vec4 res = fract(depth * bitShift);"); src.push(" res -= res.xxyz * bitMask;"); src.push(" return res;"); src.push("}"); src.push("out vec4 outColor;"); src.push("void main(void) {"); if (clipping) { src.push(" bool clippable = (int(vFlags) >> 16 & 0xF) == 1;"); src.push(" if (clippable) {"); src.push(" float dist = 0.0;"); for (let i = 0; i < sectionPlanesState.getNumAllocatedSectionPlanes(); i++) { src.push("if (sectionPlaneActive" + i + ") {"); src.push(" dist += clamp(dot(-sectionPlaneDir" + i + ".xyz, vWorldPosition.xyz - sectionPlanePos" + i + ".xyz), 0.0, 1000.0);"); src.push("}"); } src.push("if (dist > 0.0) { discard; }"); src.push("}"); } if (scene.logarithmicDepthBufferEnabled) { src.push(" gl_FragDepth = isPerspective == 0.0 ? gl_FragCoord.z : log2( vFragDepth ) * logDepthBufFC * 0.5;"); } src.push(" float zNormalizedDepth = abs((pickZNear + vViewPosition.z) / (pickZFar - pickZNear));"); src.push(" outColor = packDepth(zNormalizedDepth); "); // Must be linear depth src.push("}"); return src; } } /** * @private */ class TrianglesPickNormalsRenderer extends TrianglesInstancingRenderer { _buildVertexShader() { const scene = this._scene; const sectionPlanesState = scene._sectionPlanesState; const clipping = sectionPlanesState.getNumAllocatedSectionPlanes() > 0; const src = []; src.push("#version 300 es"); src.push("// Instancing geometry normals vertex shader"); src.push("uniform int renderPass;"); src.push("in vec3 position;"); if (scene.entityOffsetsEnabled) { src.push("in vec3 offset;"); } src.push("in vec2 normal;"); src.push("in float flags;"); src.push("in vec4 modelMatrixCol0;"); // Modeling matrix src.push("in vec4 modelMatrixCol1;"); src.push("in vec4 modelMatrixCol2;"); src.push("in vec4 modelNormalMatrixCol0;"); src.push("in vec4 modelNormalMatrixCol1;"); src.push("in vec4 modelNormalMatrixCol2;"); src.push("uniform bool pickInvisible;"); this._addMatricesUniformBlockLines(src); this._addRemapClipPosLines(src, 3); if (scene.logarithmicDepthBufferEnabled) { src.push("uniform float logDepthBufFC;"); src.push("out float vFragDepth;"); src.push("bool isPerspectiveMatrix(mat4 m) {"); src.push(" return (m[2][3] == - 1.0);"); src.push("}"); src.push("out float isPerspective;"); } src.push("vec3 octDecode(vec2 oct) {"); src.push(" vec3 v = vec3(oct.xy, 1.0 - abs(oct.x) - abs(oct.y));"); src.push(" if (v.z < 0.0) {"); src.push(" v.xy = (1.0 - abs(v.yx)) * vec2(v.x >= 0.0 ? 1.0 : -1.0, v.y >= 0.0 ? 1.0 : -1.0);"); src.push(" }"); src.push(" return normalize(v);"); src.push("}"); if (clipping) { src.push("out vec4 vWorldPosition;"); src.push("out float vFlags;"); } src.push("out vec3 vWorldNormal;"); src.push("void main(void) {"); // pickFlag = NOT_RENDERED | PICK // renderPass = PICK src.push(`int pickFlag = int(flags) >> 12 & 0xF;`); src.push(`if (pickFlag != renderPass) {`); src.push(" gl_Position = vec4(0.0, 0.0, 0.0, 0.0);"); // Cull vertex src.push("} else {"); src.push(" vec4 worldPosition = positionsDecodeMatrix * vec4(position, 1.0); "); src.push(" worldPosition = worldMatrix * vec4(dot(worldPosition, modelMatrixCol0), dot(worldPosition, modelMatrixCol1), dot(worldPosition, modelMatrixCol2), 1.0);"); if (scene.entityOffsetsEnabled) { src.push(" worldPosition.xyz = worldPosition.xyz + offset;"); } src.push(" vec4 viewPosition = viewMatrix * worldPosition; "); src.push(" vec4 modelNormal = vec4(octDecode(normal.xy), 0.0); "); src.push(" vec3 worldNormal = vec3(dot(modelNormal, modelNormalMatrixCol0), dot(modelNormal, modelNormalMatrixCol1), dot(modelNormal, modelNormalMatrixCol2));"); src.push(" vWorldNormal = worldNormal;"); if (clipping) { src.push(" vWorldPosition = worldPosition;"); src.push("vFlags = flags;"); } src.push("vec4 clipPos = projMatrix * viewPosition;"); if (scene.logarithmicDepthBufferEnabled) { src.push("vFragDepth = 1.0 + clipPos.w;"); src.push("isPerspective = float (isPerspectiveMatrix(projMatrix));"); } src.push("gl_Position = remapClipPos(clipPos);"); src.push("}"); src.push("}"); return src; } _buildFragmentShader() { const scene = this._scene; const sectionPlanesState = scene._sectionPlanesState; const clipping = sectionPlanesState.getNumAllocatedSectionPlanes() > 0; const src = []; src.push("#version 300 es"); src.push("// Batched geometry normals fragment shader"); src.push("#ifdef GL_FRAGMENT_PRECISION_HIGH"); src.push("precision highp float;"); src.push("precision highp int;"); src.push("#else"); src.push("precision mediump float;"); src.push("precision mediump int;"); src.push("#endif"); if (scene.logarithmicDepthBufferEnabled) { src.push("in float isPerspective;"); src.push("uniform float logDepthBufFC;"); src.push("in float vFragDepth;"); } if (clipping) { src.push("in vec4 vWorldPosition;"); src.push("in float vFlags;"); for (let i = 0; i < sectionPlanesState.getNumAllocatedSectionPlanes(); i++) { src.push("uniform bool sectionPlaneActive" + i + ";"); src.push("uniform vec3 sectionPlanePos" + i + ";"); src.push("uniform vec3 sectionPlaneDir" + i + ";"); } } src.push("in vec3 vWorldNormal;"); src.push("out highp ivec4 outNormal;"); src.push("void main(void) {"); if (clipping) { src.push(" bool clippable = (int(vFlags) >> 16 & 0xF) == 1;"); src.push(" if (clippable) {"); src.push(" float dist = 0.0;"); for (var i = 0; i < sectionPlanesState.getNumAllocatedSectionPlanes(); i++) { src.push("if (sectionPlaneActive" + i + ") {"); src.push(" dist += clamp(dot(-sectionPlaneDir" + i + ".xyz, vWorldPosition.xyz - sectionPlanePos" + i + ".xyz), 0.0, 1000.0);"); src.push("}"); } src.push("if (dist > 0.0) { discard; }"); src.push("}"); } if (scene.logarithmicDepthBufferEnabled) { src.push(" gl_FragDepth = isPerspective == 0.0 ? gl_FragCoord.z : log2( vFragDepth ) * logDepthBufFC * 0.5;"); } src.push(` outNormal = ivec4(vWorldNormal * float(${math.MAX_INT}), 1.0);`); src.push("}"); return src; } } /** * @private */ class TrianglesOcclusionRenderer extends TrianglesInstancingRenderer { _buildVertexShader() { const scene = this._scene; const sectionPlanesState = scene._sectionPlanesState; const clipping = sectionPlanesState.getNumAllocatedSectionPlanes() > 0; const src = []; src.push("#version 300 es"); src.push("// TrianglesInstancingOcclusionRenderer vertex shader"); src.push("uniform int renderPass;"); src.push("in vec3 position;"); if (scene.entityOffsetsEnabled) { src.push("in vec3 offset;"); } src.push("in vec4 color;"); src.push("in float flags;"); src.push("in vec4 modelMatrixCol0;"); // Modeling matrix src.push("in vec4 modelMatrixCol1;"); src.push("in vec4 modelMatrixCol2;"); this._addMatricesUniformBlockLines(src); if (clipping) { src.push("out vec4 vWorldPosition;"); src.push("out float vFlags;"); } src.push("void main(void) {"); // colorFlag = NOT_RENDERED | COLOR_OPAQUE | COLOR_TRANSPARENT // renderPass = COLOR_OPAQUE | COLOR_TRANSPARENT src.push(`int colorFlag = int(flags) & 0xF;`); src.push(`if (colorFlag != renderPass) {`); src.push(" gl_Position = vec4(0.0, 0.0, 0.0, 0.0);"); src.push("} else {"); src.push(" vec4 worldPosition = positionsDecodeMatrix * vec4(position, 1.0); "); src.push(" worldPosition = worldMatrix * vec4(dot(worldPosition, modelMatrixCol0), dot(worldPosition, modelMatrixCol1), dot(worldPosition, modelMatrixCol2), 1.0);"); if (scene.entityOffsetsEnabled) { src.push(" worldPosition.xyz = worldPosition.xyz + offset;"); } src.push(" vec4 viewPosition = viewMatrix * worldPosition; "); if (clipping) { src.push(" vWorldPosition = worldPosition;"); src.push("vFlags = flags;"); } src.push("vec4 clipPos = projMatrix * viewPosition;"); src.push("gl_Position = clipPos;"); src.push("}"); src.push("}"); return src; } _buildFragmentShader() { const scene = this._scene; const sectionPlanesState = scene._sectionPlanesState; const clipping = sectionPlanesState.getNumAllocatedSectionPlanes() > 0; const src = []; src.push("#version 300 es"); src.push("// TrianglesInstancingOcclusionRenderer fragment shader"); src.push("#ifdef GL_FRAGMENT_PRECISION_HIGH"); src.push("precision highp float;"); src.push("precision highp int;"); src.push("#else"); src.push("precision mediump float;"); src.push("precision mediump int;"); src.push("#endif"); if (clipping) { src.push("in vec4 vWorldPosition;"); src.push("in float vFlags;"); for (let i = 0; i < sectionPlanesState.getNumAllocatedSectionPlanes(); i++) { src.push("uniform bool sectionPlaneActive" + i + ";"); src.push("uniform vec3 sectionPlanePos" + i + ";"); src.push("uniform vec3 sectionPlaneDir" + i + ";"); } } src.push("out vec4 outColor;"); src.push("void main(void) {"); if (clipping) { src.push(" bool clippable = (int(vFlags) >> 16 & 0xF) == 1;"); src.push(" if (clippable) {"); src.push(" float dist = 0.0;"); for (let i = 0; i < sectionPlanesState.getNumAllocatedSectionPlanes(); i++) { src.push("if (sectionPlaneActive" + i + ") {"); src.push(" dist += clamp(dot(-sectionPlaneDir" + i + ".xyz, vWorldPosition.xyz - sectionPlanePos" + i + ".xyz), 0.0, 1000.0);"); src.push("}"); } src.push("if (dist > 0.0) { discard; }"); src.push("}"); } src.push(" outColor = vec4(0.0, 0.0, 1.0, 1.0); "); // Occluders are blue src.push("}"); return src; } } /** * @private */ class TrianglesDepthRenderer extends TrianglesInstancingRenderer { _buildVertexShader() { const scene = this._scene; const sectionPlanesState = scene._sectionPlanesState; const clipping = sectionPlanesState.getNumAllocatedSectionPlanes() > 0; const src = []; src.push("#version 300 es"); src.push("// Instancing geometry depth drawing vertex shader"); src.push("uniform int renderPass;"); src.push("in vec3 position;"); if (scene.entityOffsetsEnabled) { src.push("in vec3 offset;"); } src.push("in float flags;"); src.push("in vec4 modelMatrixCol0;"); src.push("in vec4 modelMatrixCol1;"); src.push("in vec4 modelMatrixCol2;"); this._addMatricesUniformBlockLines(src); if (scene.logarithmicDepthBufferEnabled) { src.push("uniform float logDepthBufFC;"); src.push("out float vFragDepth;"); src.push("bool isPerspectiveMatrix(mat4 m) {"); src.push(" return (m[2][3] == - 1.0);"); src.push("}"); src.push("out float isPerspective;"); } if (clipping) { src.push("out vec4 vWorldPosition;"); src.push("out float vFlags;"); } src.push("out vec2 vHighPrecisionZW;"); src.push("void main(void) {"); // colorFlag = NOT_RENDERED | COLOR_OPAQUE | COLOR_TRANSPARENT // renderPass = COLOR_OPAQUE src.push(`int colorFlag = int(flags) & 0xF;`); src.push(`if (colorFlag != renderPass) {`); src.push(" gl_Position = vec4(0.0, 0.0, 0.0, 0.0);"); // Cull vertex src.push("} else {"); src.push(" vec4 worldPosition = positionsDecodeMatrix * vec4(position, 1.0); "); src.push(" worldPosition = worldMatrix * vec4(dot(worldPosition, modelMatrixCol0), dot(worldPosition, modelMatrixCol1), dot(worldPosition, modelMatrixCol2), 1.0);"); if (scene.entityOffsetsEnabled) { src.push(" worldPosition.xyz = worldPosition.xyz + offset;"); } src.push(" vec4 viewPosition = viewMatrix * worldPosition; "); if (clipping) { src.push("vWorldPosition = worldPosition;"); src.push("vFlags = flags;"); } src.push("vec4 clipPos = projMatrix * viewPosition;"); if (scene.logarithmicDepthBufferEnabled) { src.push("vFragDepth = 1.0 + clipPos.w;"); src.push("isPerspective = float (isPerspectiveMatrix(projMatrix));"); } src.push("gl_Position = clipPos;"); src.push("vHighPrecisionZW = gl_Position.zw;"); src.push("}"); src.push("}"); return src; } _buildFragmentShader() { const scene = this._scene; const sectionPlanesState = scene._sectionPlanesState; let i; let len; const clipping = sectionPlanesState.getNumAllocatedSectionPlanes() > 0; const src = []; src.push("#version 300 es"); src.push("// Instancing geometry depth drawing fragment shader"); src.push("precision highp float;"); src.push("precision highp int;"); if (scene.logarithmicDepthBufferEnabled) { src.push("in float isPerspective;"); src.push("uniform float logDepthBufFC;"); src.push("in float vFragDepth;"); } if (clipping) { src.push("in vec4 vWorldPosition;"); src.push("in float vFlags;"); for (i = 0, len = sectionPlanesState.getNumAllocatedSectionPlanes(); i < len; i++) { src.push("uniform bool sectionPlaneActive" + i + ";"); src.push("uniform vec3 sectionPlanePos" + i + ";"); src.push("uniform vec3 sectionPlaneDir" + i + ";"); } } src.push("in vec2 vHighPrecisionZW;"); src.push("out vec4 outColor;"); src.push("void main(void) {"); if (clipping) { src.push(" bool clippable = (int(vFlags) >> 16 & 0xF) == 1;"); src.push(" if (clippable) {"); src.push(" float dist = 0.0;"); for (i = 0, len = sectionPlanesState.getNumAllocatedSectionPlanes(); i < len; i++) { src.push("if (sectionPlaneActive" + i + ") {"); src.push(" dist += clamp(dot(-sectionPlaneDir" + i + ".xyz, vWorldPosition.xyz - sectionPlanePos" + i + ".xyz), 0.0, 1000.0);"); src.push("}"); } src.push("if (dist > 0.0) { discard; }"); src.push("}"); } if (scene.logarithmicDepthBufferEnabled) { src.push(" gl_FragDepth = isPerspective == 0.0 ? gl_FragCoord.z : log2( vFragDepth ) * logDepthBufFC * 0.5;"); } src.push("float fragCoordZ = 0.5 * vHighPrecisionZW[0] / vHighPrecisionZW[1] + 0.5;"); src.push(" outColor = vec4(vec3(1.0 - fragCoordZ), 1.0); "); src.push("}"); return src; } } /** * @private */ class TrianglesNormalsRenderer extends TrianglesInstancingRenderer { _buildVertexShader() { const scene = this._scene; const sectionPlanesState = scene._sectionPlanesState; const clipping = sectionPlanesState.getNumAllocatedSectionPlanes() > 0; const src = []; src.push("#version 300 es"); src.push("// Instancing geometry normals drawing vertex shader"); src.push("uniform int renderPass;"); src.push("in vec3 position;"); if (scene.entityOffsetsEnabled) { src.push("in vec3 offset;"); } src.push("in vec3 normal;"); src.push("in vec4 color;"); src.push("in float flags;"); src.push("in vec4 modelMatrixCol0;"); src.push("in vec4 modelMatrixCol1;"); src.push("in vec4 modelMatrixCol2;"); this._addMatricesUniformBlockLines(src, true); if (scene.logarithmicDepthBufferEnabled) { src.push("uniform float logDepthBufFC;"); src.push("out float vFragDepth;"); src.push("bool isPerspectiveMatrix(mat4 m) {"); src.push(" return (m[2][3] == - 1.0);"); src.push("}"); src.push("out float isPerspective;"); } src.push("vec3 octDecode(vec2 oct) {"); src.push(" vec3 v = vec3(oct.xy, 1.0 - abs(oct.x) - abs(oct.y));"); src.push(" if (v.z < 0.0) {"); src.push(" v.xy = (1.0 - abs(v.yx)) * vec2(v.x >= 0.0 ? 1.0 : -1.0, v.y >= 0.0 ? 1.0 : -1.0);"); src.push(" }"); src.push(" return normalize(v);"); src.push("}"); if (clipping) { src.push("out vec4 vWorldPosition;"); src.push("out float vFlags;"); } src.push("out vec3 vViewNormal;"); src.push("void main(void) {"); // colorFlag = NOT_RENDERED | COLOR_OPAQUE | COLOR_TRANSPARENT // renderPass = COLOR_OPAQUE src.push(`int colorFlag = int(flags) & 0xF;`); src.push(`if (colorFlag != renderPass) {`); src.push(" gl_Position = vec4(0.0, 0.0, 0.0, 0.0);"); src.push("} else {"); src.push(" vec4 worldPosition = positionsDecodeMatrix * vec4(position, 1.0); "); src.push(" worldPosition = worldMatrix * vec4(dot(worldPosition, modelMatrixCol0), dot(worldPosition, modelMatrixCol1), dot(worldPosition, modelMatrixCol2), 1.0);"); if (scene.entityOffsetsEnabled) { src.push(" worldPosition.xyz = worldPosition.xyz + offset;"); } src.push(" vec4 viewPosition = viewMatrix * worldPosition; "); src.push(" vec4 worldNormal = worldNormalMatrix * vec4(octDecode(normal.xy), 0.0); "); src.push(" vec3 viewNormal = normalize((viewNormalMatrix * worldNormal).xyz);"); if (clipping) { src.push("vWorldPosition = worldPosition;"); src.push("vFlags = flags;"); } src.push(" vViewNormal = viewNormal;"); src.push("vec4 clipPos = projMatrix * viewPosition;"); if (scene.logarithmicDepthBufferEnabled) { src.push("vFragDepth = 1.0 + clipPos.w;"); src.push("isPerspective = float (isPerspectiveMatrix(projMatrix));"); } src.push("gl_Position = clipPos;"); src.push("}"); src.push("}"); return src; } _buildFragmentShader() { const scene = this._scene; const sectionPlanesState = scene._sectionPlanesState; const clipping = sectionPlanesState.getNumAllocatedSectionPlanes() > 0; const src = []; src.push("#version 300 es"); src.push("// Instancing geometry depth drawing fragment shader"); src.push("#ifdef GL_FRAGMENT_PRECISION_HIGH"); src.push("precision highp float;"); src.push("precision highp int;"); src.push("#else"); src.push("precision mediump float;"); src.push("precision mediump int;"); src.push("#endif"); if (scene.logarithmicDepthBufferEnabled) { src.push("in float isPerspective;"); src.push("uniform float logDepthBufFC;"); src.push("in float vFragDepth;"); } if (clipping) { src.push("in vec4 vWorldPosition;"); src.push("in float vFlags;"); for (let i = 0, len = sectionPlanesState.getNumAllocatedSectionPlanes(); i < len; i++) { src.push("uniform bool sectionPlaneActive" + i + ";"); src.push("uniform vec3 sectionPlanePos" + i + ";"); src.push("uniform vec3 sectionPlaneDir" + i + ";"); } } src.push("in vec3 vViewNormal;"); src.push("vec3 packNormalToRGB( const in vec3 normal ) {"); src.push(" return normalize( normal ) * 0.5 + 0.5;"); src.push("}"); src.push("out vec4 outColor;"); src.push("void main(void) {"); if (clipping) { src.push(" bool clippable = (int(vFlags) >> 16 & 0xF) == 1;"); src.push(" if (clippable) {"); src.push(" float dist = 0.0;"); for (let i = 0, len = sectionPlanesState.getNumAllocatedSectionPlanes(); i < len; i++) { src.push("if (sectionPlaneActive" + i + ") {"); src.push(" dist += clamp(dot(-sectionPlaneDir" + i + ".xyz, vWorldPosition.xyz - sectionPlanePos" + i + ".xyz), 0.0, 1000.0);"); src.push("}"); } src.push("if (dist > 0.0) { discard; }"); src.push("}"); } if (scene.logarithmicDepthBufferEnabled) { src.push(" gl_FragDepth = isPerspective == 0.0 ? gl_FragCoord.z : log2( vFragDepth ) * logDepthBufFC * 0.5;"); } src.push(" outColor = vec4(packNormalToRGB(vViewNormal), 1.0); "); src.push("}"); return src; } } /** * Renders InstancingLayer fragment depths to a shadow map. * * @private */ class TrianglesShadowRenderer extends TrianglesInstancingRenderer { _buildVertexShader() { const scene = this._scene; const sectionPlanesState = scene._sectionPlanesState; const clipping = sectionPlanesState.getNumAllocatedSectionPlanes() > 0; const src = []; src.push("#version 300 es"); src.push("// Instancing geometry shadow drawing vertex shader"); src.push("in vec3 position;"); if (scene.entityOffsetsEnabled) { src.push("in vec3 offset;"); } src.push("in vec4 color;"); src.push("in float flags;"); src.push("in vec4 modelMatrixCol0;"); src.push("in vec4 modelMatrixCol1;"); src.push("in vec4 modelMatrixCol2;"); src.push("uniform mat4 shadowViewMatrix;"); src.push("uniform mat4 shadowProjMatrix;"); this._addMatricesUniformBlockLines(src); if (clipping) { src.push("out vec4 vWorldPosition;"); src.push("out float vFlags;"); } src.push("void main(void) {"); src.push(`int colorFlag = int(flags) & 0xF;`); src.push("bool visible = (colorFlag > 0);"); src.push("bool transparent = ((float(color.a) / 255.0) < 1.0);"); src.push(`if (!visible || transparent) {`); src.push(" gl_Position = vec4(0.0, 0.0, 0.0, 0.0);"); // Cull vertex src.push("} else {"); src.push(" vec4 worldPosition = positionsDecodeMatrix * vec4(position, 1.0); "); src.push(" worldPosition = vec4(dot(worldPosition, modelMatrixCol0), dot(worldPosition, modelMatrixCol1), dot(worldPosition, modelMatrixCol2), 1.0);"); if (scene.entityOffsetsEnabled) { src.push(" worldPosition.xyz = worldPosition.xyz + offset;"); } src.push(" vec4 viewPosition = shadowViewMatrix * worldPosition; "); if (clipping) { src.push("vWorldPosition = worldPosition;"); src.push("vFlags = flags;"); } src.push(" gl_Position = shadowProjMatrix * viewPosition;"); src.push("}"); src.push("}"); return src; } _buildFragmentShader() { const scene = this._scene; const sectionPlanesState = scene._sectionPlanesState; const clipping = sectionPlanesState.getNumAllocatedSectionPlanes() > 0; const src = []; src.push("#version 300 es"); src.push("// Instancing geometry depth drawing fragment shader"); src.push("#ifdef GL_FRAGMENT_PRECISION_HIGH"); src.push("precision highp float;"); src.push("precision highp int;"); src.push("#else"); src.push("precision mediump float;"); src.push("precision mediump int;"); src.push("#endif"); if (scene.logarithmicDepthBufferEnabled) { src.push("uniform float logDepthBufFC;"); src.push("in float vFragDepth;"); } if (clipping) { src.push("in vec4 vWorldPosition;"); src.push("in float vFlags;"); for (let i = 0, len = sectionPlanesState.getNumAllocatedSectionPlanes(); i < len; i++) { src.push("uniform bool sectionPlaneActive" + i + ";"); src.push("uniform vec3 sectionPlanePos" + i + ";"); src.push("uniform vec3 sectionPlaneDir" + i + ";"); } } src.push("in vec3 vViewNormal;"); src.push("vec3 packNormalToRGB( const in vec3 normal ) {"); src.push(" return normalize( normal ) * 0.5 + 0.5;"); src.push("}"); src.push("out vec4 outColor;"); src.push("void main(void) {"); if (clipping) { src.push(" bool clippable = (int(vFlags) >> 16 & 0xF) == 1;"); src.push(" if (clippable) {"); src.push(" float dist = 0.0;"); for (let i = 0, len = sectionPlanesState.getNumAllocatedSectionPlanes(); i < len; i++) { src.push("if (sectionPlaneActive" + i + ") {"); src.push(" dist += clamp(dot(-sectionPlaneDir" + i + ".xyz, vWorldPosition.xyz - sectionPlanePos" + i + ".xyz), 0.0, 1000.0);"); src.push("}"); } src.push("if (dist > 0.0) { discard; }"); src.push("}"); } if (scene.logarithmicDepthBufferEnabled) { src.push("gl_FragDepth = log2( vFragDepth ) * logDepthBufFC * 0.5;"); } src.push(" outColor = vec4(packNormalToRGB(vViewNormal), 1.0); "); src.push("}"); return src; } } const TEXTURE_DECODE_FUNCS = {}; TEXTURE_DECODE_FUNCS[LinearEncoding] = "linearToLinear"; TEXTURE_DECODE_FUNCS[sRGBEncoding] = "sRGBToLinear"; /** * @private */ class TrianglesPBRRenderer extends TrianglesInstancingRenderer { _getHash() { const scene = this._scene; return [scene.gammaOutput, scene._lightsState.getHash(), scene._sectionPlanesState.getHash(), (this._withSAO ? "sao" : "nosao")].join(";"); } drawLayer(frameCtx, layer, renderPass) { super.drawLayer(frameCtx, layer, renderPass, { incrementDrawState: true }); } _buildVertexShader() { const scene = this._scene; const sectionPlanesState = scene._sectionPlanesState; const lightsState = scene._lightsState; const clipping = sectionPlanesState.getNumAllocatedSectionPlanes() > 0; const clippingCaps = sectionPlanesState.clippingCaps; const src = []; src.push("#version 300 es"); src.push("// Instancing geometry quality drawing vertex shader"); src.push("uniform int renderPass;"); src.push("in vec3 position;"); src.push("in vec3 normal;"); src.push("in vec4 color;"); src.push("in vec2 uv;"); src.push("in vec2 metallicRoughness;"); src.push("in float flags;"); if (scene.entityOffsetsEnabled) { src.push("in vec3 offset;"); } src.push("in vec4 modelMatrixCol0;"); // Modeling matrix src.push("in vec4 modelMatrixCol1;"); src.push("in vec4 modelMatrixCol2;"); src.push("in vec4 modelNormalMatrixCol0;"); src.push("in vec4 modelNormalMatrixCol1;"); src.push("in vec4 modelNormalMatrixCol2;"); this._addMatricesUniformBlockLines(src, true); src.push("uniform mat3 uvDecodeMatrix;"); if (scene.logarithmicDepthBufferEnabled) { src.push("uniform float logDepthBufFC;"); src.push("out float vFragDepth;"); src.push("bool isPerspectiveMatrix(mat4 m) {"); src.push(" return (m[2][3] == - 1.0);"); src.push("}"); src.push("out float isPerspective;"); } src.push("vec3 octDecode(vec2 oct) {"); src.push(" vec3 v = vec3(oct.xy, 1.0 - abs(oct.x) - abs(oct.y));"); src.push(" if (v.z < 0.0) {"); src.push(" v.xy = (1.0 - abs(v.yx)) * vec2(v.x >= 0.0 ? 1.0 : -1.0, v.y >= 0.0 ? 1.0 : -1.0);"); src.push(" }"); src.push(" return normalize(v);"); src.push("}"); src.push("out vec4 vViewPosition;"); src.push("out vec3 vViewNormal;"); src.push("out vec4 vColor;"); src.push("out vec2 vUV;"); src.push("out vec2 vMetallicRoughness;"); if (lightsState.lightMaps.length > 0) { src.push("out vec3 vWorldNormal;"); } if (clipping) { src.push("out vec4 vWorldPosition;"); src.push("out float vFlags;"); if (clippingCaps) { src.push("out vec4 vClipPosition;"); } } src.push("void main(void) {"); // colorFlag = NOT_RENDERED | COLOR_OPAQUE | COLOR_TRANSPARENT // renderPass = COLOR_OPAQUE | COLOR_TRANSPARENT src.push(`int colorFlag = int(flags) & 0xF;`); src.push(`if (colorFlag != renderPass) {`); src.push(" gl_Position = vec4(0.0, 0.0, 0.0, 0.0);"); // Cull vertex src.push("} else {"); src.push("vec4 worldPosition = positionsDecodeMatrix * vec4(position, 1.0); "); src.push("worldPosition = worldMatrix * vec4(dot(worldPosition, modelMatrixCol0), dot(worldPosition, modelMatrixCol1), dot(worldPosition, modelMatrixCol2), 1.0);"); if (scene.entityOffsetsEnabled) { src.push(" worldPosition.xyz = worldPosition.xyz + offset;"); } src.push("vec4 viewPosition = viewMatrix * worldPosition; "); src.push("vec4 modelNormal = vec4(octDecode(normal.xy), 0.0); "); src.push("vec4 worldNormal = worldNormalMatrix * vec4(dot(modelNormal, modelNormalMatrixCol0), dot(modelNormal, modelNormalMatrixCol1), dot(modelNormal, modelNormalMatrixCol2), 1.0);"); src.push("vec3 viewNormal = vec4(viewNormalMatrix * worldNormal).xyz;"); src.push("vec4 clipPos = projMatrix * viewPosition;"); if (scene.logarithmicDepthBufferEnabled) { src.push("vFragDepth = 1.0 + clipPos.w;"); src.push("isPerspective = float (isPerspectiveMatrix(projMatrix));"); } if (clipping) { src.push("vWorldPosition = worldPosition;"); src.push("vFlags = flags;"); if (clippingCaps) { src.push("vClipPosition = clipPos;"); } } src.push("vViewPosition = viewPosition;"); src.push("vViewNormal = viewNormal;"); src.push("vColor = color;"); src.push("vUV = (uvDecodeMatrix * vec3(uv, 1.0)).xy;"); src.push("vMetallicRoughness = metallicRoughness;"); if (lightsState.lightMaps.length > 0) { src.push("vWorldNormal = worldNormal.xyz;"); } src.push("gl_Position = clipPos;"); src.push("}"); src.push("}"); return src; } _buildFragmentShader() { const scene = this._scene; const gammaOutput = scene.gammaOutput; // If set, then it expects that all textures and colors need to be outputted in premultiplied gamma. Default is false. const sectionPlanesState = scene._sectionPlanesState; const lightsState = scene._lightsState; const clipping = sectionPlanesState.getNumAllocatedSectionPlanes() > 0; const clippingCaps = sectionPlanesState.clippingCaps; const src = []; src.push("#version 300 es"); src.push("// Instancing geometry quality drawing fragment shader"); src.push("#ifdef GL_FRAGMENT_PRECISION_HIGH"); src.push("precision highp float;"); src.push("precision highp int;"); src.push("#else"); src.push("precision mediump float;"); src.push("precision mediump int;"); src.push("#endif"); if (scene.logarithmicDepthBufferEnabled) { src.push("in float isPerspective;"); src.push("uniform float logDepthBufFC;"); src.push("in float vFragDepth;"); } src.push("uniform sampler2D uColorMap;"); src.push("uniform sampler2D uMetallicRoughMap;"); src.push("uniform sampler2D uEmissiveMap;"); src.push("uniform sampler2D uNormalMap;"); if (this._withSAO) { src.push("uniform sampler2D uOcclusionTexture;"); src.push("uniform vec4 uSAOParams;"); src.push("const float packUpscale = 256. / 255.;"); src.push("const float unpackDownScale = 255. / 256.;"); src.push("const vec3 packFactors = vec3( 256. * 256. * 256., 256. * 256., 256. );"); src.push("const vec4 unPackFactors = unpackDownScale / vec4( packFactors, 1. );"); src.push("float unpackRGBToFloat( const in vec4 v ) {"); src.push(" return dot( v, unPackFactors );"); src.push("}"); } if (lightsState.reflectionMaps.length > 0) { src.push("uniform samplerCube reflectionMap;"); } if (lightsState.lightMaps.length > 0) { src.push("uniform samplerCube lightMap;"); } src.push("uniform vec4 lightAmbient;"); for (let i = 0, len = lightsState.lights.length; i < len; i++) { const light = lightsState.lights[i]; if (light.type === "ambient") { continue; } src.push("uniform vec4 lightColor" + i + ";"); if (light.type === "dir") { src.push("uniform vec3 lightDir" + i + ";"); } if (light.type === "point") { src.push("uniform vec3 lightPos" + i + ";"); } if (light.type === "spot") { src.push("uniform vec3 lightPos" + i + ";"); src.push("uniform vec3 lightDir" + i + ";"); } } src.push("uniform float gammaFactor;"); src.push("vec4 linearToLinear( in vec4 value ) {"); src.push(" return value;"); src.push("}"); src.push("vec4 sRGBToLinear( in vec4 value ) {"); src.push(" return vec4( mix( pow( value.rgb * 0.9478672986 + vec3( 0.0521327014 ), vec3( 2.4 ) ), value.rgb * 0.0773993808, vec3( lessThanEqual( value.rgb, vec3( 0.04045 ) ) ) ), value.w );"); src.push("}"); src.push("vec4 gammaToLinear( in vec4 value) {"); src.push(" return vec4( pow( value.xyz, vec3( gammaFactor ) ), value.w );"); src.push("}"); if (gammaOutput) { src.push("vec4 linearToGamma( in vec4 value, in float gammaFactor ) {"); src.push(" return vec4( pow( value.xyz, vec3( 1.0 / gammaFactor ) ), value.w );"); src.push("}"); } if (clipping) { src.push("in vec4 vWorldPosition;"); src.push("in float vFlags;"); if (clippingCaps) { src.push("in vec4 vClipPosition;"); } for (let i = 0, len = sectionPlanesState.getNumAllocatedSectionPlanes(); i < len; i++) { src.push("uniform bool sectionPlaneActive" + i + ";"); src.push("uniform vec3 sectionPlanePos" + i + ";"); src.push("uniform vec3 sectionPlaneDir" + i + ";"); } } src.push("in vec4 vViewPosition;"); src.push("in vec3 vViewNormal;"); src.push("in vec4 vColor;"); src.push("in vec2 vUV;"); src.push("in vec2 vMetallicRoughness;"); if (lightsState.lightMaps.length > 0) { src.push("in vec3 vWorldNormal;"); } this._addMatricesUniformBlockLines(src, true); // CONSTANT DEFINITIONS src.push("#define PI 3.14159265359"); src.push("#define RECIPROCAL_PI 0.31830988618"); src.push("#define RECIPROCAL_PI2 0.15915494"); src.push("#define EPSILON 1e-6"); src.push("#define saturate(a) clamp( a, 0.0, 1.0 )"); // UTILITY DEFINITIONS src.push("vec3 perturbNormal2Arb( vec3 eye_pos, vec3 surf_norm, vec2 uv ) {"); src.push(" vec3 texel = texture( uNormalMap, uv ).xyz;"); src.push(" if (texel.r == 0.0 && texel.g == 0.0 && texel.b == 0.0) {"); src.push(" return normalize(surf_norm );"); src.push(" }"); src.push(" vec3 q0 = vec3( dFdx( eye_pos.x ), dFdx( eye_pos.y ), dFdx( eye_pos.z ) );"); src.push(" vec3 q1 = vec3( dFdy( eye_pos.x ), dFdy( eye_pos.y ), dFdy( eye_pos.z ) );"); src.push(" vec2 st0 = dFdx( uv.st );"); src.push(" vec2 st1 = dFdy( uv.st );"); src.push(" vec3 S = normalize( q0 * st1.t - q1 * st0.t );"); src.push(" vec3 T = normalize( -q0 * st1.s + q1 * st0.s );"); src.push(" vec3 N = normalize( surf_norm );"); src.push(" vec3 mapN = texel.xyz * 2.0 - 1.0;"); src.push(" mat3 tsn = mat3( S, T, N );"); // src.push(" mapN *= 3.0;"); src.push(" return normalize( tsn * mapN );"); src.push("}"); src.push("vec3 inverseTransformDirection(in vec3 dir, in mat4 matrix) {"); src.push(" return normalize( ( vec4( dir, 0.0 ) * matrix ).xyz );"); src.push("}"); // STRUCTURES src.push("struct IncidentLight {"); src.push(" vec3 color;"); src.push(" vec3 direction;"); src.push("};"); src.push("struct ReflectedLight {"); src.push(" vec3 diffuse;"); src.push(" vec3 specular;"); src.push("};"); src.push("struct Geometry {"); src.push(" vec3 position;"); src.push(" vec3 viewNormal;"); src.push(" vec3 worldNormal;"); src.push(" vec3 viewEyeDir;"); src.push("};"); src.push("struct Material {"); src.push(" vec3 diffuseColor;"); src.push(" float specularRoughness;"); src.push(" vec3 specularColor;"); src.push(" float shine;"); // Only used for Phong src.push("};"); // IRRADIANCE EVALUATION src.push("float GGXRoughnessToBlinnExponent(const in float ggxRoughness) {"); src.push(" float r = ggxRoughness + 0.0001;"); src.push(" return (2.0 / (r * r) - 2.0);"); src.push("}"); src.push("float getSpecularMIPLevel(const in float blinnShininessExponent, const in int maxMIPLevel) {"); src.push(" float maxMIPLevelScalar = float( maxMIPLevel );"); src.push(" float desiredMIPLevel = maxMIPLevelScalar - 0.79248 - 0.5 * log2( ( blinnShininessExponent * blinnShininessExponent ) + 1.0 );"); src.push(" return clamp( desiredMIPLevel, 0.0, maxMIPLevelScalar );"); src.push("}"); if (lightsState.reflectionMaps.length > 0) { src.push("vec3 getLightProbeIndirectRadiance(const in vec3 reflectVec, const in float blinnShininessExponent, const in int maxMIPLevel) {"); src.push(" float mipLevel = 0.5 * getSpecularMIPLevel(blinnShininessExponent, maxMIPLevel);"); //TODO: a random factor - fix this src.push(" vec3 envMapColor = " + TEXTURE_DECODE_FUNCS[lightsState.reflectionMaps[0].encoding] + "(texture(reflectionMap, reflectVec, mipLevel)).rgb;"); src.push(" return envMapColor;"); src.push("}"); } // SPECULAR BRDF EVALUATION src.push("vec3 F_Schlick(const in vec3 specularColor, const in float dotLH) {"); src.push(" float fresnel = exp2( ( -5.55473 * dotLH - 6.98316 ) * dotLH );"); src.push(" return ( 1.0 - specularColor ) * fresnel + specularColor;"); src.push("}"); src.push("float G_GGX_Smith(const in float alpha, const in float dotNL, const in float dotNV) {"); src.push(" float a2 = ( alpha * alpha );"); src.push(" float gl = dotNL + sqrt( a2 + ( 1.0 - a2 ) * ( dotNL * dotNL ) );"); src.push(" float gv = dotNV + sqrt( a2 + ( 1.0 - a2 ) * ( dotNV * dotNV ) );"); src.push(" return 1.0 / ( gl * gv );"); src.push("}"); src.push("float G_GGX_SmithCorrelated(const in float alpha, const in float dotNL, const in float dotNV) {"); src.push(" float a2 = ( alpha * alpha );"); src.push(" float gv = dotNL * sqrt( a2 + ( 1.0 - a2 ) * ( dotNV * dotNV ) );"); src.push(" float gl = dotNV * sqrt( a2 + ( 1.0 - a2 ) * ( dotNL * dotNL ) );"); src.push(" return 0.5 / max( gv + gl, EPSILON );"); src.push("}"); src.push("float D_GGX(const in float alpha, const in float dotNH) {"); src.push(" float a2 = ( alpha * alpha );"); src.push(" float denom = ( dotNH * dotNH) * ( a2 - 1.0 ) + 1.0;"); src.push(" return RECIPROCAL_PI * a2 / ( denom * denom);"); src.push("}"); src.push("vec3 BRDF_Specular_GGX(const in IncidentLight incidentLight, const in Geometry geometry, const in vec3 specularColor, const in float roughness) {"); src.push(" float alpha = ( roughness * roughness );"); src.push(" vec3 halfDir = normalize( incidentLight.direction + geometry.viewEyeDir );"); src.push(" float dotNL = saturate( dot( geometry.viewNormal, incidentLight.direction ) );"); src.push(" float dotNV = saturate( dot( geometry.viewNormal, geometry.viewEyeDir ) );"); src.push(" float dotNH = saturate( dot( geometry.viewNormal, halfDir ) );"); src.push(" float dotLH = saturate( dot( incidentLight.direction, halfDir ) );"); src.push(" vec3 F = F_Schlick( specularColor, dotLH );"); src.push(" float G = G_GGX_SmithCorrelated( alpha, dotNL, dotNV );"); src.push(" float D = D_GGX( alpha, dotNH );"); src.push(" return F * (G * D);"); src.push("}"); src.push("vec3 BRDF_Specular_GGX_Environment(const in Geometry geometry, const in vec3 specularColor, const in float roughness) {"); src.push(" float dotNV = saturate(dot(geometry.viewNormal, geometry.viewEyeDir));"); src.push(" const vec4 c0 = vec4( -1, -0.0275, -0.572, 0.022);"); src.push(" const vec4 c1 = vec4( 1, 0.0425, 1.04, -0.04);"); src.push(" vec4 r = roughness * c0 + c1;"); src.push(" float a004 = min(r.x * r.x, exp2(-9.28 * dotNV)) * r.x + r.y;"); src.push(" vec2 AB = vec2(-1.04, 1.04) * a004 + r.zw;"); src.push(" return specularColor * AB.x + AB.y;"); src.push("}"); if (lightsState.lightMaps.length > 0 || lightsState.reflectionMaps.length > 0) { src.push("void computePBRLightMapping(const in Geometry geometry, const in Material material, inout ReflectedLight reflectedLight) {"); if (lightsState.lightMaps.length > 0) { src.push(" vec3 irradiance = " + TEXTURE_DECODE_FUNCS[lightsState.lightMaps[0].encoding] + "(texture(lightMap, geometry.worldNormal)).rgb;"); src.push(" irradiance *= PI;"); src.push(" vec3 diffuseBRDFContrib = (RECIPROCAL_PI * material.diffuseColor);"); src.push(" reflectedLight.diffuse += irradiance * diffuseBRDFContrib;"); } if (lightsState.reflectionMaps.length > 0) { src.push(" vec3 reflectVec = reflect(geometry.viewEyeDir, geometry.viewNormal);"); src.push(" reflectVec = inverseTransformDirection(reflectVec, viewMatrix);"); src.push(" float blinnExpFromRoughness = GGXRoughnessToBlinnExponent(material.specularRoughness);"); src.push(" vec3 radiance = getLightProbeIndirectRadiance(reflectVec, blinnExpFromRoughness, 8);"); src.push(" vec3 specularBRDFContrib = BRDF_Specular_GGX_Environment(geometry, material.specularColor, material.specularRoughness);"); src.push(" reflectedLight.specular += radiance * specularBRDFContrib;"); } src.push("}"); } // MAIN LIGHTING COMPUTATION FUNCTION src.push("void computePBRLighting(const in IncidentLight incidentLight, const in Geometry geometry, const in Material material, inout ReflectedLight reflectedLight) {"); src.push(" float dotNL = saturate(dot(geometry.viewNormal, incidentLight.direction));"); src.push(" vec3 irradiance = dotNL * incidentLight.color * PI;"); src.push(" reflectedLight.diffuse += irradiance * (RECIPROCAL_PI * material.diffuseColor);"); src.push(" reflectedLight.specular += irradiance * BRDF_Specular_GGX(incidentLight, geometry, material.specularColor, material.specularRoughness);"); src.push("}"); src.push("out vec4 outColor;"); src.push("void main(void) {"); if (clipping) { src.push(" bool clippable = (int(vFlags) >> 16 & 0xF) == 1;"); src.push(" if (clippable) {"); src.push(" float dist = 0.0;"); for (let i = 0, len = sectionPlanesState.getNumAllocatedSectionPlanes(); i < len; i++) { src.push("if (sectionPlaneActive" + i + ") {"); src.push(" dist += clamp(dot(-sectionPlaneDir" + i + ".xyz, vWorldPosition.xyz - sectionPlanePos" + i + ".xyz), 0.0, 1000.0);"); src.push("}"); } if (clippingCaps) { src.push(" if (dist > (0.002 * vClipPosition.w)) {"); src.push(" discard;"); src.push(" }"); src.push(" if (dist > 0.0) { "); src.push(" outColor=vec4(1.0, 0.0, 0.0, 1.0);"); if (scene.logarithmicDepthBufferEnabled) { src.push(" gl_FragDepth = log2( vFragDepth ) * logDepthBufFC * 0.5;"); } src.push(" return;"); src.push("}"); } else { src.push(" if (dist > 0.0) { "); src.push(" discard;"); src.push(" }"); } src.push("}"); } src.push("IncidentLight light;"); src.push("Material material;"); src.push("Geometry geometry;"); src.push("ReflectedLight reflectedLight = ReflectedLight(vec3(0.0,0.0,0.0), vec3(0.0,0.0,0.0));"); src.push("vec3 rgb = (vec3(float(vColor.r) / 255.0, float(vColor.g) / 255.0, float(vColor.b) / 255.0));"); src.push("float opacity = float(vColor.a) / 255.0;"); src.push("vec3 baseColor = rgb;"); src.push("float specularF0 = 1.0;"); src.push("float metallic = float(vMetallicRoughness.r) / 255.0;"); src.push("float roughness = float(vMetallicRoughness.g) / 255.0;"); src.push("float dielectricSpecular = 0.16 * specularF0 * specularF0;"); src.push("vec4 colorTexel = sRGBToLinear(texture(uColorMap, vUV));"); src.push("baseColor *= colorTexel.rgb;"); // src.push("opacity = colorTexel.a;"); src.push("vec3 metalRoughTexel = texture(uMetallicRoughMap, vUV).rgb;"); src.push("metallic *= metalRoughTexel.b;"); src.push("roughness *= metalRoughTexel.g;"); src.push("vec3 viewNormal = perturbNormal2Arb( vViewPosition.xyz, normalize(vViewNormal), vUV );"); src.push("material.diffuseColor = baseColor * (1.0 - dielectricSpecular) * (1.0 - metallic);"); src.push("material.specularRoughness = clamp(roughness, 0.04, 1.0);"); src.push("material.specularColor = mix(vec3(dielectricSpecular), baseColor, metallic);"); src.push("geometry.position = vViewPosition.xyz;"); src.push("geometry.viewNormal = -normalize(viewNormal);"); src.push("geometry.viewEyeDir = normalize(vViewPosition.xyz);"); if (lightsState.lightMaps.length > 0) { src.push("geometry.worldNormal = normalize(vWorldNormal);"); } if (lightsState.lightMaps.length > 0 || lightsState.reflectionMaps.length > 0) { src.push("computePBRLightMapping(geometry, material, reflectedLight);"); } for (let i = 0, len = lightsState.lights.length; i < len; i++) { const light = lightsState.lights[i]; if (light.type === "ambient") { continue; } if (light.type === "dir") { if (light.space === "view") { src.push("light.direction = normalize(lightDir" + i + ");"); } else { src.push("light.direction = normalize((viewMatrix * vec4(lightDir" + i + ", 0.0)).xyz);"); } } else if (light.type === "point") { if (light.space === "view") { src.push("light.direction = normalize(lightPos" + i + " - vViewPosition.xyz);"); } else { src.push("light.direction = normalize((viewMatrix * vec4(lightPos" + i + ", 0.0)).xyz);"); } } else if (light.type === "spot") { if (light.space === "view") { src.push("light.direction = normalize(lightDir" + i + ");"); } else { src.push("light.direction = normalize((viewMatrix * vec4(lightDir" + i + ", 0.0)).xyz);"); } } else { continue; } src.push("light.color = lightColor" + i + ".rgb * lightColor" + i + ".a;"); // a is intensity src.push("computePBRLighting(light, geometry, material, reflectedLight);"); } src.push("vec3 emissiveColor = sRGBToLinear(texture(uEmissiveMap, vUV)).rgb;"); // TODO: correct gamma function src.push("vec3 outgoingLight = (lightAmbient.rgb * lightAmbient.a * baseColor * opacity * rgb) + (reflectedLight.diffuse) + (reflectedLight.specular) + emissiveColor;"); src.push("vec4 fragColor;"); if (this._withSAO) { // Doing SAO blend in the main solid fill draw shader just so that edge lines can be drawn over the top // Would be more efficient to defer this, then render lines later, using same depth buffer for Z-reject src.push(" float viewportWidth = uSAOParams[0];"); src.push(" float viewportHeight = uSAOParams[1];"); src.push(" float blendCutoff = uSAOParams[2];"); src.push(" float blendFactor = uSAOParams[3];"); src.push(" vec2 uv = vec2(gl_FragCoord.x / viewportWidth, gl_FragCoord.y / viewportHeight);"); src.push(" float ambient = smoothstep(blendCutoff, 1.0, unpackRGBToFloat(texture(uOcclusionTexture, uv))) * blendFactor;"); src.push(" fragColor = vec4(outgoingLight.rgb * ambient, opacity);"); } else { src.push(" fragColor = vec4(outgoingLight.rgb, opacity);"); } if (gammaOutput) { src.push("fragColor = linearToGamma(fragColor, gammaFactor);"); } src.push("outColor = fragColor;"); if (scene.logarithmicDepthBufferEnabled) { src.push(" gl_FragDepth = isPerspective == 0.0 ? gl_FragCoord.z : log2( vFragDepth ) * logDepthBufFC * 0.5;"); } src.push("}"); return src; } } /** * @private */ class TrianglesPickNormalsFlatRenderer extends TrianglesInstancingRenderer { _buildVertexShader() { const scene = this._scene; const sectionPlanesState = scene._sectionPlanesState; const clipping = sectionPlanesState.getNumAllocatedSectionPlanes() > 0; const src = []; src.push("#version 300 es"); src.push("// Instancing geometry normals vertex shader"); src.push("uniform int renderPass;"); src.push("in vec3 position;"); if (scene.entityOffsetsEnabled) { src.push("in vec3 offset;"); } src.push("in float flags;"); src.push("in vec4 modelMatrixCol0;"); // Modeling matrix src.push("in vec4 modelMatrixCol1;"); src.push("in vec4 modelMatrixCol2;"); src.push("uniform bool pickInvisible;"); this._addMatricesUniformBlockLines(src); this._addRemapClipPosLines(src, 3); if (scene.logarithmicDepthBufferEnabled) { src.push("uniform float logDepthBufFC;"); src.push("out float vFragDepth;"); src.push("bool isPerspectiveMatrix(mat4 m) {"); src.push(" return (m[2][3] == - 1.0);"); src.push("}"); src.push("out float isPerspective;"); } if (clipping) { src.push("out float vFlags;"); } src.push("out vec4 vWorldPosition;"); src.push("void main(void) {"); // pickFlag = NOT_RENDERED | PICK // renderPass = PICK src.push(`int pickFlag = int(flags) >> 12 & 0xF;`); src.push(`if (pickFlag != renderPass) {`); src.push(" gl_Position = vec4(0.0, 0.0, 0.0, 0.0);"); // Cull vertex src.push("} else {"); src.push(" vec4 worldPosition = positionsDecodeMatrix * vec4(position, 1.0); "); src.push(" worldPosition = worldMatrix * vec4(dot(worldPosition, modelMatrixCol0), dot(worldPosition, modelMatrixCol1), dot(worldPosition, modelMatrixCol2), 1.0);"); if (scene.entityOffsetsEnabled) { src.push(" worldPosition.xyz = worldPosition.xyz + offset;"); } src.push(" vec4 viewPosition = viewMatrix * worldPosition; "); src.push(" vWorldPosition = worldPosition;"); src.push("vec4 clipPos = projMatrix * viewPosition;"); if (scene.logarithmicDepthBufferEnabled) { src.push("vFragDepth = 1.0 + clipPos.w;"); src.push("isPerspective = float (isPerspectiveMatrix(projMatrix));"); } if (clipping) { src.push("vFlags = flags;"); } src.push("gl_Position = remapClipPos(clipPos);"); src.push("}"); src.push("}"); return src; } _buildFragmentShader() { const scene = this._scene; const sectionPlanesState = scene._sectionPlanesState; const clipping = sectionPlanesState.getNumAllocatedSectionPlanes() > 0; const src = []; src.push("#version 300 es"); src.push("// Batched geometry normals fragment shader"); src.push("#ifdef GL_FRAGMENT_PRECISION_HIGH"); src.push("precision highp float;"); src.push("precision highp int;"); src.push("#else"); src.push("precision mediump float;"); src.push("precision mediump int;"); src.push("#endif"); if (scene.logarithmicDepthBufferEnabled) { src.push("in float isPerspective;"); src.push("uniform float logDepthBufFC;"); src.push("in float vFragDepth;"); } src.push("in vec4 vWorldPosition;"); if (clipping) { src.push("in float vFlags;"); for (let i = 0; i < sectionPlanesState.getNumAllocatedSectionPlanes(); i++) { src.push("uniform bool sectionPlaneActive" + i + ";"); src.push("uniform vec3 sectionPlanePos" + i + ";"); src.push("uniform vec3 sectionPlaneDir" + i + ";"); } } src.push("out highp ivec4 outNormal;"); src.push("void main(void) {"); if (clipping) { src.push(" bool clippable = (int(vFlags) >> 16 & 0xF) == 1;"); src.push(" if (clippable) {"); src.push(" float dist = 0.0;"); for (var i = 0; i < sectionPlanesState.getNumAllocatedSectionPlanes(); i++) { src.push("if (sectionPlaneActive" + i + ") {"); src.push(" dist += clamp(dot(-sectionPlaneDir" + i + ".xyz, vWorldPosition.xyz - sectionPlanePos" + i + ".xyz), 0.0, 1000.0);"); src.push("}"); } src.push("if (dist > 0.0) { discard; }"); src.push("}"); } if (scene.logarithmicDepthBufferEnabled) { src.push(" gl_FragDepth = isPerspective == 0.0 ? gl_FragCoord.z : log2( vFragDepth ) * logDepthBufFC * 0.5;"); } src.push(" vec3 xTangent = dFdx( vWorldPosition.xyz );"); src.push(" vec3 yTangent = dFdy( vWorldPosition.xyz );"); src.push(" vec3 worldNormal = normalize( cross( xTangent, yTangent ) );"); src.push(` outNormal = ivec4(worldNormal * float(${math.MAX_INT}), 1.0);`); src.push("}"); return src; } } /** * @private */ class TrianglesColorTextureRenderer extends TrianglesInstancingRenderer { _getHash() { const scene = this._scene; return [scene.gammaOutput, scene._lightsState.getHash(), scene._sectionPlanesState.getHash(), (this._withSAO ? "sao" : "nosao")].join(";"); } drawLayer(frameCtx, layer, renderPass) { super.drawLayer(frameCtx, layer, renderPass, { incrementDrawState: true }); } _buildVertexShader() { const scene = this._scene; const sectionPlanesState = scene._sectionPlanesState; const clipping = sectionPlanesState.getNumAllocatedSectionPlanes() > 0; const src = []; src.push("#version 300 es"); src.push("// Instancing geometry drawing vertex shader"); src.push("uniform int renderPass;"); src.push("in vec3 position;"); src.push("in vec4 color;"); src.push("in vec2 uv;"); src.push("in float flags;"); if (scene.entityOffsetsEnabled) { src.push("in vec3 offset;"); } src.push("in vec4 modelMatrixCol0;"); // Modeling matrix src.push("in vec4 modelMatrixCol1;"); src.push("in vec4 modelMatrixCol2;"); this._addMatricesUniformBlockLines(src); src.push("uniform mat3 uvDecodeMatrix;"); if (scene.logarithmicDepthBufferEnabled) { src.push("uniform float logDepthBufFC;"); src.push("out float vFragDepth;"); src.push("bool isPerspectiveMatrix(mat4 m) {"); src.push(" return (m[2][3] == - 1.0);"); src.push("}"); src.push("out float isPerspective;"); } if (clipping) { src.push("out vec4 vWorldPosition;"); src.push("out float vFlags;"); } src.push("out vec4 vViewPosition;"); src.push("out vec4 vColor;"); src.push("out vec2 vUV;"); src.push("void main(void) {"); // colorFlag = NOT_RENDERED | COLOR_OPAQUE | COLOR_TRANSPARENT // renderPass = COLOR_OPAQUE | COLOR_TRANSPARENT src.push(`int colorFlag = int(flags) & 0xF;`); src.push(`if (colorFlag != renderPass) {`); src.push(" gl_Position = vec4(0.0, 0.0, 0.0, 0.0);"); // Cull vertex src.push("} else {"); src.push("vec4 worldPosition = positionsDecodeMatrix * vec4(position, 1.0); "); src.push("worldPosition = worldMatrix * vec4(dot(worldPosition, modelMatrixCol0), dot(worldPosition, modelMatrixCol1), dot(worldPosition, modelMatrixCol2), 1.0);"); if (scene.entityOffsetsEnabled) { src.push("worldPosition.xyz = worldPosition.xyz + offset;"); } src.push("vec4 viewPosition = viewMatrix * worldPosition; "); src.push("vViewPosition = viewPosition;"); src.push("vColor = vec4(float(color.r) / 255.0, float(color.g) / 255.0, float(color.b) / 255.0, float(color.a) / 255.0);"); src.push("vUV = (uvDecodeMatrix * vec3(uv, 1.0)).xy;"); src.push("vec4 clipPos = projMatrix * viewPosition;"); if (scene.logarithmicDepthBufferEnabled) { src.push("vFragDepth = 1.0 + clipPos.w;"); src.push("isPerspective = float (isPerspectiveMatrix(projMatrix));"); } if (clipping) { src.push("vWorldPosition = worldPosition;"); src.push("vFlags = flags;"); } src.push("gl_Position = clipPos;"); src.push("}"); src.push("}"); return src; } _buildFragmentShader() { const scene = this._scene; const gammaOutput = scene.gammaOutput; // If set, then it expects that all textures and colors need to be outputted in premultiplied gamma. Default is false. const lightsState = scene._lightsState; const sectionPlanesState = scene._sectionPlanesState; const clipping = sectionPlanesState.getNumAllocatedSectionPlanes() > 0; const useAlphaCutoff = this._useAlphaCutoff; const src = []; src.push("#version 300 es"); src.push("// Instancing geometry drawing fragment shader"); src.push("#ifdef GL_FRAGMENT_PRECISION_HIGH"); src.push("precision highp float;"); src.push("precision highp int;"); src.push("#else"); src.push("precision mediump float;"); src.push("precision mediump int;"); src.push("#endif"); if (scene.logarithmicDepthBufferEnabled) { src.push("in float isPerspective;"); src.push("uniform float logDepthBufFC;"); src.push("in float vFragDepth;"); } src.push("uniform sampler2D uColorMap;"); if (this._withSAO) { src.push("uniform sampler2D uOcclusionTexture;"); src.push("uniform vec4 uSAOParams;"); src.push("const float packUpscale = 256. / 255.;"); src.push("const float unpackDownScale = 255. / 256.;"); src.push("const vec3 packFactors = vec3( 256. * 256. * 256., 256. * 256., 256. );"); src.push("const vec4 unPackFactors = unpackDownScale / vec4( packFactors, 1. );"); src.push("float unpackRGBToFloat( const in vec4 v ) {"); src.push(" return dot( v, unPackFactors );"); src.push("}"); } src.push("uniform float gammaFactor;"); src.push("vec4 linearToLinear( in vec4 value ) {"); src.push(" return value;"); src.push("}"); src.push("vec4 sRGBToLinear( in vec4 value ) {"); src.push(" return vec4( mix( pow( value.rgb * 0.9478672986 + vec3( 0.0521327014 ), vec3( 2.4 ) ), value.rgb * 0.0773993808, vec3( lessThanEqual( value.rgb, vec3( 0.04045 ) ) ) ), value.w );"); src.push("}"); src.push("vec4 gammaToLinear( in vec4 value) {"); src.push(" return vec4( pow( value.xyz, vec3( gammaFactor ) ), value.w );"); src.push("}"); if (gammaOutput) { src.push("vec4 linearToGamma( in vec4 value, in float gammaFactor ) {"); src.push(" return vec4( pow( value.xyz, vec3( 1.0 / gammaFactor ) ), value.w );"); src.push("}"); } if (clipping) { src.push("in vec4 vWorldPosition;"); src.push("in float vFlags;"); for (let i = 0, len = sectionPlanesState.getNumAllocatedSectionPlanes(); i < len; i++) { src.push("uniform bool sectionPlaneActive" + i + ";"); src.push("uniform vec3 sectionPlanePos" + i + ";"); src.push("uniform vec3 sectionPlaneDir" + i + ";"); } src.push("uniform float sliceThickness;"); src.push("uniform vec4 sliceColor;"); } this._addMatricesUniformBlockLines(src); src.push("uniform vec4 lightAmbient;"); for (let i = 0, len = lightsState.lights.length; i < len; i++) { const light = lightsState.lights[i]; if (light.type === "ambient") { continue; } src.push("uniform vec4 lightColor" + i + ";"); if (light.type === "dir") { src.push("uniform vec3 lightDir" + i + ";"); } if (light.type === "point") { src.push("uniform vec3 lightPos" + i + ";"); } if (light.type === "spot") { src.push("uniform vec3 lightPos" + i + ";"); src.push("uniform vec3 lightDir" + i + ";"); } } if (useAlphaCutoff) { src.push("uniform float materialAlphaCutoff;"); } src.push("in vec4 vViewPosition;"); src.push("in vec4 vColor;"); src.push("in vec2 vUV;"); src.push("out vec4 outColor;"); src.push("void main(void) {"); src.push(" vec4 newColor;"); src.push(" newColor = vColor;"); if (clipping) { src.push(" bool clippable = (int(vFlags) >> 16 & 0xF) == 1;"); src.push(" if (clippable) {"); src.push(" float dist = 0.0;"); for (let i = 0, len = sectionPlanesState.getNumAllocatedSectionPlanes(); i < len; i++) { src.push("if (sectionPlaneActive" + i + ") {"); src.push(" dist += clamp(dot(-sectionPlaneDir" + i + ".xyz, vWorldPosition.xyz - sectionPlanePos" + i + ".xyz), 0.0, 1000.0);"); src.push("}"); } src.push(" if (dist > sliceThickness) { "); src.push(" discard;"); src.push(" }"); src.push(" if (dist > 0.0) { "); src.push(" newColor = sliceColor;"); src.push(" }"); src.push("}"); } src.push("vec3 reflectedColor = vec3(0.0, 0.0, 0.0);"); src.push("vec3 viewLightDir = vec3(0.0, 0.0, -1.0);"); src.push("float lambertian = 1.0;"); src.push("vec3 xTangent = dFdx( vViewPosition.xyz );"); src.push("vec3 yTangent = dFdy( vViewPosition.xyz );"); src.push("vec3 viewNormal = normalize( cross( xTangent, yTangent ) );"); for (let i = 0, len = lightsState.lights.length; i < len; i++) { const light = lightsState.lights[i]; if (light.type === "ambient") { continue; } if (light.type === "dir") { if (light.space === "view") { src.push("viewLightDir = normalize(lightDir" + i + ");"); } else { src.push("viewLightDir = normalize((viewMatrix * vec4(lightDir" + i + ", 0.0)).xyz);"); } } else if (light.type === "point") { if (light.space === "view") { src.push("viewLightDir = -normalize(lightPos" + i + " - viewPosition.xyz);"); } else { src.push("viewLightDir = -normalize((viewMatrix * vec4(lightPos" + i + ", 0.0)).xyz);"); } } else if (light.type === "spot") { if (light.space === "view") { src.push("viewLightDir = normalize(lightDir" + i + ");"); } else { src.push("viewLightDir = normalize((viewMatrix * vec4(lightDir" + i + ", 0.0)).xyz);"); } } else { continue; } src.push("lambertian = max(dot(-viewNormal, viewLightDir), 0.0);"); src.push("reflectedColor += lambertian * (lightColor" + i + ".rgb * lightColor" + i + ".a);"); } src.push("vec4 color = vec4((lightAmbient.rgb * lightAmbient.a * newColor.rgb) + (reflectedColor * newColor.rgb), newColor.a);"); src.push("vec4 sampleColor = texture(uColorMap, vUV);"); if (useAlphaCutoff) { src.push("if (sampleColor.a < materialAlphaCutoff) {"); src.push(" discard;"); src.push("}"); } if (gammaOutput) { src.push("sampleColor = sRGBToLinear(sampleColor);"); } src.push("vec4 colorTexel = color * sampleColor;"); src.push("float opacity = color.a;"); if (this._withSAO) { // Doing SAO blend in the main solid fill draw shader just so that edge lines can be drawn over the top // Would be more efficient to defer this, then render lines later, using same depth buffer for Z-reject src.push(" float viewportWidth = uSAOParams[0];"); src.push(" float viewportHeight = uSAOParams[1];"); src.push(" float blendCutoff = uSAOParams[2];"); src.push(" float blendFactor = uSAOParams[3];"); src.push(" vec2 uv = vec2(gl_FragCoord.x / viewportWidth, gl_FragCoord.y / viewportHeight);"); src.push(" float ambient = smoothstep(blendCutoff, 1.0, unpackRGBToFloat(texture(uOcclusionTexture, uv))) * blendFactor;"); src.push(" outColor = vec4(colorTexel.rgb * ambient, opacity);"); } else { src.push(" outColor = vec4(colorTexel.rgb, opacity);"); } if (gammaOutput) { src.push("outColor = linearToGamma(outColor, gammaFactor);"); } if (scene.logarithmicDepthBufferEnabled) { src.push("gl_FragDepth = isPerspective == 0.0 ? gl_FragCoord.z : log2( vFragDepth ) * logDepthBufFC * 0.5;"); } src.push("}"); return src; } } const tempVec3a$B = math.vec3(); const tempVec3b$x = math.vec3(); const tempVec3c$s = math.vec3(); const tempVec3d$b = math.vec3(); const tempMat4a$o = math.mat4(); /** * @private */ class TrianglesSnapInitRenderer extends VBORenderer { constructor(scene) { super(scene, false, { instancing: true }); } drawLayer(frameCtx, instancingLayer, renderPass) { if (!this._program) { this._allocate(); if (this.errors) { return; } } if (frameCtx.lastProgramId !== this._program.id) { frameCtx.lastProgramId = this._program.id; this._bindProgram(); } const model = instancingLayer.model; const scene = model.scene; const gl = scene.canvas.gl; const camera = scene.camera; const state = instancingLayer._state; const origin = instancingLayer._state.origin; const {position, rotationMatrix} = model; const aabb = instancingLayer.aabb; // Per-layer AABB for best RTC accuracy const viewMatrix = frameCtx.pickViewMatrix || camera.viewMatrix; if (this._vaoCache.has(instancingLayer)) { gl.bindVertexArray(this._vaoCache.get(instancingLayer)); } else { this._vaoCache.set(instancingLayer, this._makeVAO(state)); } const coordinateScaler = tempVec3a$B; coordinateScaler[0] = math.safeInv(aabb[3] - aabb[0]) * math.MAX_INT; coordinateScaler[1] = math.safeInv(aabb[4] - aabb[1]) * math.MAX_INT; coordinateScaler[2] = math.safeInv(aabb[5] - aabb[2]) * math.MAX_INT; frameCtx.snapPickCoordinateScale[0] = math.safeInv(coordinateScaler[0]); frameCtx.snapPickCoordinateScale[1] = math.safeInv(coordinateScaler[1]); frameCtx.snapPickCoordinateScale[2] = math.safeInv(coordinateScaler[2]); let rtcViewMatrix; let rtcCameraEye; if (origin || position[0] !== 0 || position[1] !== 0 || position[2] !== 0) { const rtcOrigin = tempVec3b$x; if (origin) { const rotatedOrigin = math.transformPoint3(rotationMatrix, origin, tempVec3c$s); rtcOrigin[0] = rotatedOrigin[0]; rtcOrigin[1] = rotatedOrigin[1]; rtcOrigin[2] = rotatedOrigin[2]; } else { rtcOrigin[0] = 0; rtcOrigin[1] = 0; rtcOrigin[2] = 0; } rtcOrigin[0] += position[0]; rtcOrigin[1] += position[1]; rtcOrigin[2] += position[2]; rtcViewMatrix = createRTCViewMat(viewMatrix, rtcOrigin, tempMat4a$o); rtcCameraEye = tempVec3d$b; rtcCameraEye[0] = camera.eye[0] - rtcOrigin[0]; rtcCameraEye[1] = camera.eye[1] - rtcOrigin[1]; rtcCameraEye[2] = camera.eye[2] - rtcOrigin[2]; frameCtx.snapPickOrigin[0] = rtcOrigin[0]; frameCtx.snapPickOrigin[1] = rtcOrigin[1]; frameCtx.snapPickOrigin[2] = rtcOrigin[2]; } else { rtcViewMatrix = viewMatrix; rtcCameraEye = camera.eye; frameCtx.snapPickOrigin[0] = 0; frameCtx.snapPickOrigin[1] = 0; frameCtx.snapPickOrigin[2] = 0; } gl.uniform3fv(this._uCameraEyeRtc, rtcCameraEye); gl.uniform2fv(this.uVectorA, frameCtx.snapVectorA); gl.uniform2fv(this.uInverseVectorAB, frameCtx.snapInvVectorAB); gl.uniform1i(this._uLayerNumber, frameCtx.snapPickLayerNumber); gl.uniform3fv(this._uCoordinateScaler, coordinateScaler); gl.uniform1i(this._uRenderPass, renderPass); gl.uniform1i(this._uPickInvisible, frameCtx.pickInvisible); let offset = 0; const mat4Size = 4 * 4; this._matricesUniformBlockBufferData.set(rotationMatrix, 0); this._matricesUniformBlockBufferData.set(rtcViewMatrix, offset += mat4Size); this._matricesUniformBlockBufferData.set(camera.projMatrix, offset += mat4Size); this._matricesUniformBlockBufferData.set(state.positionsDecodeMatrix, offset += mat4Size); gl.bindBuffer(gl.UNIFORM_BUFFER, this._matricesUniformBlockBuffer); gl.bufferData(gl.UNIFORM_BUFFER, this._matricesUniformBlockBufferData, gl.DYNAMIC_DRAW); gl.bindBufferBase( gl.UNIFORM_BUFFER, this._matricesUniformBlockBufferBindingPoint, this._matricesUniformBlockBuffer); { const logDepthBufFC = 2.0 / (Math.log(frameCtx.pickZFar + 1.0) / Math.LN2); gl.uniform1f(this._uLogDepthBufFC, logDepthBufFC); } this.setSectionPlanesStateUniforms(instancingLayer); state.indicesBuf.bind(); gl.drawElementsInstanced(gl.TRIANGLES, state.indicesBuf.numItems, state.indicesBuf.itemType, 0, state.numInstances); state.indicesBuf.unbind(); gl.bindVertexArray(null); } _allocate() { super._allocate(); const program = this._program; { this._uLogDepthBufFC = program.getLocation("logDepthBufFC"); } this._uCameraEyeRtc = program.getLocation("uCameraEyeRtc"); this.uVectorA = program.getLocation("snapVectorA"); this.uInverseVectorAB = program.getLocation("snapInvVectorAB"); this._uLayerNumber = program.getLocation("layerNumber"); this._uCoordinateScaler = program.getLocation("coordinateScaler"); } _bindProgram() { this._program.bind(); } _buildVertexShader() { const scene = this._scene; const sectionPlanesState = scene._sectionPlanesState; const clipping = sectionPlanesState.getNumAllocatedSectionPlanes() > 0; const src = []; src.push ('#version 300 es'); src.push("// SnapInstancingDepthBufInitRenderer vertex shader"); src.push("#ifdef GL_FRAGMENT_PRECISION_HIGH"); src.push("precision highp float;"); src.push("precision highp int;"); src.push("precision highp usampler2D;"); src.push("precision highp isampler2D;"); src.push("precision highp sampler2D;"); src.push("#else"); src.push("precision mediump float;"); src.push("precision mediump int;"); src.push("precision mediump usampler2D;"); src.push("precision mediump isampler2D;"); src.push("precision mediump sampler2D;"); src.push("#endif"); src.push("uniform int renderPass;"); src.push("in vec4 pickColor;"); src.push("in vec3 position;"); if (scene.entityOffsetsEnabled) { src.push("in vec3 offset;"); } src.push("in float flags;"); src.push("in vec4 modelMatrixCol0;"); // Modeling matrix src.push("in vec4 modelMatrixCol1;"); src.push("in vec4 modelMatrixCol2;"); src.push("uniform bool pickInvisible;"); this._addMatricesUniformBlockLines(src); src.push("uniform vec3 uCameraEyeRtc;"); src.push("uniform vec2 snapVectorA;"); src.push("uniform vec2 snapInvVectorAB;"); { src.push("uniform float logDepthBufFC;"); src.push("out float vFragDepth;"); src.push("bool isPerspectiveMatrix(mat4 m) {"); src.push(" return (m[2][3] == - 1.0);"); src.push("}"); src.push("out float isPerspective;"); } src.push("vec2 remapClipPos(vec2 clipPos) {"); src.push(" float x = (clipPos.x - snapVectorA.x) * snapInvVectorAB.x;"); src.push(" float y = (clipPos.y - snapVectorA.y) * snapInvVectorAB.y;"); src.push(" return vec2(x, y);"); src.push("}"); src.push("flat out vec4 vPickColor;"); src.push("out vec4 vWorldPosition;"); if (clipping) { src.push("out float vFlags;"); } src.push("out highp vec3 relativeToOriginPosition;"); src.push("void main(void) {"); // pickFlag = NOT_RENDERED | PICK // renderPass = PICK src.push(`int pickFlag = int(flags) >> 12 & 0xF;`); src.push(`if (pickFlag != renderPass) {`); src.push(" gl_Position = vec4(0.0, 0.0, 0.0, 0.0);"); // Cull vertex src.push("} else {"); src.push(" vec4 worldPosition = positionsDecodeMatrix * vec4(position, 1.0); "); src.push(" worldPosition = worldMatrix * vec4(dot(worldPosition, modelMatrixCol0), dot(worldPosition, modelMatrixCol1), dot(worldPosition, modelMatrixCol2), 1.0);"); if (scene.entityOffsetsEnabled) { src.push(" worldPosition.xyz = worldPosition.xyz + offset;"); } src.push("relativeToOriginPosition = worldPosition.xyz;"); src.push(" vec4 viewPosition = viewMatrix * worldPosition; "); src.push(" vWorldPosition = worldPosition;"); if (clipping) { src.push(" vFlags = flags;"); } src.push("vPickColor = pickColor;"); src.push("vec4 clipPos = projMatrix * viewPosition;"); src.push("float tmp = clipPos.w;"); src.push("clipPos.xyzw /= tmp;"); src.push("clipPos.xy = remapClipPos(clipPos.xy);"); src.push("clipPos.xyzw *= tmp;"); { src.push("vFragDepth = 1.0 + clipPos.w;"); src.push("isPerspective = float (isPerspectiveMatrix(projMatrix));"); } src.push("gl_Position = clipPos;"); src.push("}"); src.push("}"); return src; } _buildFragmentShader() { const scene = this._scene; const sectionPlanesState = scene._sectionPlanesState; const clipping = sectionPlanesState.getNumAllocatedSectionPlanes() > 0; const src = []; src.push ('#version 300 es'); src.push("// Points instancing pick depth fragment shader"); src.push("#ifdef GL_FRAGMENT_PRECISION_HIGH"); src.push("precision highp float;"); src.push("precision highp int;"); src.push("#else"); src.push("precision mediump float;"); src.push("precision mediump int;"); src.push("#endif"); { src.push("in float isPerspective;"); src.push("uniform float logDepthBufFC;"); src.push("in float vFragDepth;"); } src.push("uniform int layerNumber;"); src.push("uniform vec3 coordinateScaler;"); src.push("in vec4 vWorldPosition;"); src.push("flat in vec4 vPickColor;"); if (clipping) { src.push("in float vFlags;"); for (let i = 0; i < sectionPlanesState.getNumAllocatedSectionPlanes(); i++) { src.push("uniform bool sectionPlaneActive" + i + ";"); src.push("uniform vec3 sectionPlanePos" + i + ";"); src.push("uniform vec3 sectionPlaneDir" + i + ";"); } } src.push("in highp vec3 relativeToOriginPosition;"); src.push("layout(location = 0) out highp ivec4 outCoords;"); src.push("layout(location = 1) out highp ivec4 outNormal;"); src.push("layout(location = 2) out lowp uvec4 outPickColor;"); src.push("void main(void) {"); if (clipping) { src.push(" bool clippable = (int(vFlags) >> 16 & 0xF) == 1;"); src.push(" if (clippable) {"); src.push(" float dist = 0.0;"); for (let i = 0; i < sectionPlanesState.getNumAllocatedSectionPlanes(); i++) { src.push("if (sectionPlaneActive" + i + ") {"); src.push(" dist += clamp(dot(-sectionPlaneDir" + i + ".xyz, vWorldPosition.xyz - sectionPlanePos" + i + ".xyz), 0.0, 1000.0);"); src.push("}"); } src.push("if (dist > 0.0) { discard; }"); src.push("}"); } { src.push(" float dx = dFdx(vFragDepth);"); src.push(" float dy = dFdy(vFragDepth);"); src.push(" float diff = sqrt(dx*dx+dy*dy);"); src.push(" gl_FragDepth = isPerspective == 0.0 ? gl_FragCoord.z : log2( vFragDepth + diff ) * logDepthBufFC * 0.5;"); } src.push("outCoords = ivec4(relativeToOriginPosition.xyz*coordinateScaler.xyz, -layerNumber);"); src.push("vec3 xTangent = dFdx( vWorldPosition.xyz );"); src.push("vec3 yTangent = dFdy( vWorldPosition.xyz );"); src.push("vec3 worldNormal = normalize( cross( xTangent, yTangent ) );"); src.push(`outNormal = ivec4(worldNormal * float(${math.MAX_INT}), 1.0);`); src.push("outPickColor = uvec4(vPickColor);"); src.push("}"); return src; } webglContextRestored() { this._program = null; } destroy() { if (this._program) { this._program.destroy(); } this._program = null; } } const tempVec3a$A = math.vec3(); const tempVec3b$w = math.vec3(); const tempVec3c$r = math.vec3(); const tempVec3d$a = math.vec3(); const tempMat4a$n = math.mat4(); /** * @private */ class TrianglesSnapRenderer extends VBORenderer { constructor(scene) { super(scene, false, { instancing: true }); } drawLayer(frameCtx, instancingLayer, renderPass) { if (!this._program) { this._allocate(instancingLayer); if (this.errors) { return; } } if (frameCtx.lastProgramId !== this._program.id) { frameCtx.lastProgramId = this._program.id; this._bindProgram(); } const model = instancingLayer.model; const scene = model.scene; const camera = scene.camera; const gl = scene.canvas.gl; const state = instancingLayer._state; const origin = instancingLayer._state.origin; const {position, rotationMatrix} = model; const aabb = instancingLayer.aabb; // Per-layer AABB for best RTC accuracy const viewMatrix = frameCtx.pickViewMatrix || camera.viewMatrix; if (this._vaoCache.has(instancingLayer)) { gl.bindVertexArray(this._vaoCache.get(instancingLayer)); } else { this._vaoCache.set(instancingLayer, this._makeVAO(state)); } const coordinateScaler = tempVec3a$A; coordinateScaler[0] = math.safeInv(aabb[3] - aabb[0]) * math.MAX_INT; coordinateScaler[1] = math.safeInv(aabb[4] - aabb[1]) * math.MAX_INT; coordinateScaler[2] = math.safeInv(aabb[5] - aabb[2]) * math.MAX_INT; frameCtx.snapPickCoordinateScale[0] = math.safeInv(coordinateScaler[0]); frameCtx.snapPickCoordinateScale[1] = math.safeInv(coordinateScaler[1]); frameCtx.snapPickCoordinateScale[2] = math.safeInv(coordinateScaler[2]); let rtcViewMatrix; let rtcCameraEye; if (origin || position[0] !== 0 || position[1] !== 0 || position[2] !== 0) { const rtcOrigin = tempVec3b$w; if (origin) { const rotatedOrigin = math.transformPoint3(rotationMatrix, origin, tempVec3c$r); rtcOrigin[0] = rotatedOrigin[0]; rtcOrigin[1] = rotatedOrigin[1]; rtcOrigin[2] = rotatedOrigin[2]; } else { rtcOrigin[0] = 0; rtcOrigin[1] = 0; rtcOrigin[2] = 0; } rtcOrigin[0] += position[0]; rtcOrigin[1] += position[1]; rtcOrigin[2] += position[2]; rtcViewMatrix = createRTCViewMat(viewMatrix, rtcOrigin, tempMat4a$n); rtcCameraEye = tempVec3d$a; rtcCameraEye[0] = camera.eye[0] - rtcOrigin[0]; rtcCameraEye[1] = camera.eye[1] - rtcOrigin[1]; rtcCameraEye[2] = camera.eye[2] - rtcOrigin[2]; frameCtx.snapPickOrigin[0] = rtcOrigin[0]; frameCtx.snapPickOrigin[1] = rtcOrigin[1]; frameCtx.snapPickOrigin[2] = rtcOrigin[2]; } else { rtcViewMatrix = viewMatrix; rtcCameraEye = camera.eye; frameCtx.snapPickOrigin[0] = 0; frameCtx.snapPickOrigin[1] = 0; frameCtx.snapPickOrigin[2] = 0; } gl.uniform3fv(this._uCameraEyeRtc, rtcCameraEye); gl.uniform2fv(this.uVectorA, frameCtx.snapVectorA); gl.uniform2fv(this.uInverseVectorAB, frameCtx.snapInvVectorAB); gl.uniform1i(this._uLayerNumber, frameCtx.snapPickLayerNumber); gl.uniform3fv(this._uCoordinateScaler, coordinateScaler); gl.uniform1i(this._uRenderPass, renderPass); gl.uniform1i(this._uPickInvisible, frameCtx.pickInvisible); let offset = 0; const mat4Size = 4 * 4; this._matricesUniformBlockBufferData.set(rotationMatrix, 0); this._matricesUniformBlockBufferData.set(rtcViewMatrix, offset += mat4Size); this._matricesUniformBlockBufferData.set(camera.projMatrix, offset += mat4Size); this._matricesUniformBlockBufferData.set(state.positionsDecodeMatrix, offset += mat4Size); gl.bindBuffer(gl.UNIFORM_BUFFER, this._matricesUniformBlockBuffer); gl.bufferData(gl.UNIFORM_BUFFER, this._matricesUniformBlockBufferData, gl.DYNAMIC_DRAW); gl.bindBufferBase( gl.UNIFORM_BUFFER, this._matricesUniformBlockBufferBindingPoint, this._matricesUniformBlockBuffer); { const logDepthBufFC = 2.0 / (Math.log(frameCtx.pickZFar + 1.0) / Math.LN2); // TODO: Far from pick project matrix gl.uniform1f(this._uLogDepthBufFC, logDepthBufFC); } this.setSectionPlanesStateUniforms(instancingLayer); if (frameCtx.snapMode === "edge" ) { if (state.edgeIndicesBuf) { state.edgeIndicesBuf.bind(); gl.drawElementsInstanced(gl.LINES, state.edgeIndicesBuf.numItems, state.edgeIndicesBuf.itemType, 0, state.numInstances); state.edgeIndicesBuf.unbind(); // needed? } } else { gl.drawArraysInstanced(gl.POINTS, 0, state.positionsBuf.numItems, state.numInstances); } gl.bindVertexArray(null); } _allocate() { super._allocate(); const program = this._program; { this._uLogDepthBufFC = program.getLocation("logDepthBufFC"); } this._uCameraEyeRtc = program.getLocation("uCameraEyeRtc"); this.uVectorA = program.getLocation("snapVectorA"); this.uInverseVectorAB = program.getLocation("snapInvVectorAB"); this._uLayerNumber = program.getLocation("layerNumber"); this._uCoordinateScaler = program.getLocation("coordinateScaler"); } _bindProgram() { this._program.bind(); } _buildVertexShader() { const scene = this._scene; const sectionPlanesState = scene._sectionPlanesState; const clipping = sectionPlanesState.getNumAllocatedSectionPlanes() > 0; const src = []; src.push ('#version 300 es'); src.push("// SnapInstancingDepthRenderer vertex shader"); src.push("#ifdef GL_FRAGMENT_PRECISION_HIGH"); src.push("precision highp float;"); src.push("precision highp int;"); src.push("precision highp usampler2D;"); src.push("precision highp isampler2D;"); src.push("precision highp sampler2D;"); src.push("#else"); src.push("precision mediump float;"); src.push("precision mediump int;"); src.push("precision mediump usampler2D;"); src.push("precision mediump isampler2D;"); src.push("precision mediump sampler2D;"); src.push("#endif"); src.push("uniform int renderPass;"); src.push("in vec3 position;"); if (scene.entityOffsetsEnabled) { src.push("in vec3 offset;"); } src.push("in float flags;"); src.push("in vec4 modelMatrixCol0;"); // Modeling matrix src.push("in vec4 modelMatrixCol1;"); src.push("in vec4 modelMatrixCol2;"); src.push("uniform bool pickInvisible;"); this._addMatricesUniformBlockLines(src); src.push("uniform vec3 uCameraEyeRtc;"); src.push("uniform vec2 snapVectorA;"); src.push("uniform vec2 snapInvVectorAB;"); { src.push("uniform float logDepthBufFC;"); src.push("out float vFragDepth;"); src.push("bool isPerspectiveMatrix(mat4 m) {"); src.push(" return (m[2][3] == - 1.0);"); src.push("}"); src.push("out float isPerspective;"); } src.push("vec2 remapClipPos(vec2 clipPos) {"); src.push(" float x = (clipPos.x - snapVectorA.x) * snapInvVectorAB.x;"); src.push(" float y = (clipPos.y - snapVectorA.y) * snapInvVectorAB.y;"); src.push(" return vec2(x, y);"); src.push("}"); if (clipping) { src.push("out vec4 vWorldPosition;"); src.push("out float vFlags;"); } src.push("out highp vec3 relativeToOriginPosition;"); src.push("void main(void) {"); // pickFlag = NOT_RENDERED | PICK // renderPass = PICK src.push(`int pickFlag = int(flags) >> 12 & 0xF;`); src.push(`if (pickFlag != renderPass) {`); src.push(" gl_Position = vec4(2.0, 0.0, 0.0, 0.0);"); // Cull vertex src.push("} else {"); src.push(" vec4 worldPosition = positionsDecodeMatrix * vec4(position, 1.0); "); src.push(" worldPosition = worldMatrix * vec4(dot(worldPosition, modelMatrixCol0), dot(worldPosition, modelMatrixCol1), dot(worldPosition, modelMatrixCol2), 1.0);"); if (scene.entityOffsetsEnabled) { src.push(" worldPosition.xyz = worldPosition.xyz + offset;"); } src.push("relativeToOriginPosition = worldPosition.xyz;"); src.push(" vec4 viewPosition = viewMatrix * worldPosition; "); if (clipping) { src.push(" vWorldPosition = worldPosition;"); src.push(" vFlags = flags;"); } src.push("vec4 clipPos = projMatrix * viewPosition;"); src.push("float tmp = clipPos.w;"); src.push("clipPos.xyzw /= tmp;"); src.push("clipPos.xy = remapClipPos(clipPos.xy);"); src.push("clipPos.xyzw *= tmp;"); { src.push("vFragDepth = 1.0 + clipPos.w;"); src.push("isPerspective = float (isPerspectiveMatrix(projMatrix));"); } src.push("gl_Position = clipPos;"); src.push("gl_PointSize = 1.0;"); // Windows needs this? src.push("}"); src.push("}"); return src; } _buildFragmentShader() { const scene = this._scene; const sectionPlanesState = scene._sectionPlanesState; const clipping = sectionPlanesState.getNumAllocatedSectionPlanes() > 0; const src = []; src.push ('#version 300 es'); src.push("// SnapInstancingDepthRenderer fragment shader"); src.push("#ifdef GL_FRAGMENT_PRECISION_HIGH"); src.push("precision highp float;"); src.push("precision highp int;"); src.push("#else"); src.push("precision mediump float;"); src.push("precision mediump int;"); src.push("#endif"); { src.push("in float isPerspective;"); src.push("uniform float logDepthBufFC;"); src.push("in float vFragDepth;"); } src.push("uniform int layerNumber;"); src.push("uniform vec3 coordinateScaler;"); if (clipping) { src.push("in vec4 vWorldPosition;"); src.push("in float vFlags;"); for (let i = 0; i < sectionPlanesState.getNumAllocatedSectionPlanes(); i++) { src.push("uniform bool sectionPlaneActive" + i + ";"); src.push("uniform vec3 sectionPlanePos" + i + ";"); src.push("uniform vec3 sectionPlaneDir" + i + ";"); } } src.push("in highp vec3 relativeToOriginPosition;"); src.push("out highp ivec4 outCoords;"); src.push("void main(void) {"); if (clipping) { src.push(" bool clippable = (int(vFlags) >> 16 & 0xF) == 1;"); src.push(" if (clippable) {"); src.push(" float dist = 0.0;"); for (let i = 0; i < sectionPlanesState.getNumAllocatedSectionPlanes(); i++) { src.push("if (sectionPlaneActive" + i + ") {"); src.push(" dist += clamp(dot(-sectionPlaneDir" + i + ".xyz, vWorldPosition.xyz - sectionPlanePos" + i + ".xyz), 0.0, 1000.0);"); src.push("}"); } src.push("if (dist > 0.0) { discard; }"); src.push("}"); } { src.push(" gl_FragDepth = isPerspective == 0.0 ? gl_FragCoord.z : log2( vFragDepth ) * logDepthBufFC * 0.5;"); } src.push("outCoords = ivec4(relativeToOriginPosition.xyz*coordinateScaler.xyz, layerNumber);"); src.push("}"); return src; } webglContextRestored() { this._program = null; } destroy() { if (this._program) { this._program.destroy(); } this._program = null; } } /** * @private */ class Renderers { constructor(scene) { this._scene = scene; } _compile() { if (this._colorRenderer && (!this._colorRenderer.getValid())) { this._colorRenderer.destroy(); this._colorRenderer = null; } if (this._colorRendererWithSAO && (!this._colorRendererWithSAO.getValid())) { this._colorRendererWithSAO.destroy(); this._colorRendererWithSAO = null; } if (this._flatColorRenderer && (!this._flatColorRenderer.getValid())) { this._flatColorRenderer.destroy(); this._flatColorRenderer = null; } if (this._flatColorRendererWithSAO && (!this._flatColorRendererWithSAO.getValid())) { this._flatColorRendererWithSAO.destroy(); this._flatColorRendererWithSAO = null; } if (this._colorTextureRenderer && (!this._colorTextureRenderer.getValid())) { this._colorTextureRenderer.destroy(); this._colorTextureRenderer = null; } if (this._colorTextureRendererWithSAO && (!this._colorTextureRendererWithSAO.getValid())) { this._colorTextureRendererWithSAO.destroy(); this._colorTextureRendererWithSAO = null; } if (this._colorTextureRendererAlphaCutoff && (!this._colorTextureRendererAlphaCutoff.getValid())) { this._colorTextureRendererAlphaCutoff.destroy(); this._colorTextureRendererAlphaCutoff = null; } if (this._colorTextureRendererWithSAOAlphaCutoff && (!this._colorTextureRendererWithSAOAlphaCutoff.getValid())) { this._colorTextureRendererWithSAOAlphaCutoff.destroy(); this._colorTextureRendererWithSAOAlphaCutoff = null; } if (this._pbrRenderer && (!this._pbrRenderer.getValid())) { this._pbrRenderer.destroy(); this._pbrRenderer = null; } if (this._pbrRendererWithSAO && (!this._pbrRendererWithSAO.getValid())) { this._pbrRendererWithSAO.destroy(); this._pbrRendererWithSAO = null; } if (this._depthRenderer && (!this._depthRenderer.getValid())) { this._depthRenderer.destroy(); this._depthRenderer = null; } if (this._normalsRenderer && (!this._normalsRenderer.getValid())) { this._normalsRenderer.destroy(); this._normalsRenderer = null; } if (this._silhouetteRenderer && (!this._silhouetteRenderer.getValid())) { this._silhouetteRenderer.destroy(); this._silhouetteRenderer = null; } if (this._edgesRenderer && (!this._edgesRenderer.getValid())) { this._edgesRenderer.destroy(); this._edgesRenderer = null; } if (this._edgesColorRenderer && (!this._edgesColorRenderer.getValid())) { this._edgesColorRenderer.destroy(); this._edgesColorRenderer = null; } if (this._pickMeshRenderer && (!this._pickMeshRenderer.getValid())) { this._pickMeshRenderer.destroy(); this._pickMeshRenderer = null; } if (this._pickDepthRenderer && (!this._pickDepthRenderer.getValid())) { this._pickDepthRenderer.destroy(); this._pickDepthRenderer = null; } if (this._pickNormalsRenderer && this._pickNormalsRenderer.getValid() === false) { this._pickNormalsRenderer.destroy(); this._pickNormalsRenderer = null; } if (this._pickNormalsFlatRenderer && this._pickNormalsFlatRenderer.getValid() === false) { this._pickNormalsFlatRenderer.destroy(); this._pickNormalsFlatRenderer = null; } if (this._occlusionRenderer && this._occlusionRenderer.getValid() === false) { this._occlusionRenderer.destroy(); this._occlusionRenderer = null; } if (this._shadowRenderer && (!this._shadowRenderer.getValid())) { this._shadowRenderer.destroy(); this._shadowRenderer = null; } if (this._snapInitRenderer && (!this._snapInitRenderer.getValid())) { this._snapInitRenderer.destroy(); this._snapInitRenderer = null; } if (this._snapRenderer && (!this._snapRenderer.getValid())) { this._snapRenderer.destroy(); this._snapRenderer = null; } } eagerCreateRenders() { // Pre-initialize certain renderers that would otherwise be lazy-initialised // on user interaction, such as picking or emphasis, so that there is no delay // when user first begins interacting with the viewer. if (!this._silhouetteRenderer) { // Used for highlighting and selection this._silhouetteRenderer = new TrianglesSilhouetteRenderer(this._scene); } if (!this._pickMeshRenderer) { this._pickMeshRenderer = new TrianglesPickMeshRenderer(this._scene); } if (!this._pickDepthRenderer) { this._pickDepthRenderer = new TrianglesPickDepthRenderer(this._scene); } if (!this._snapInitRenderer) { this._snapInitRenderer = new TrianglesSnapInitRenderer(this._scene, false); } if (!this._snapRenderer) { this._snapRenderer = new TrianglesSnapRenderer(this._scene); } } get colorRenderer() { if (!this._colorRenderer) { this._colorRenderer = new TrianglesColorRenderer(this._scene, false); } return this._colorRenderer; } get colorRendererWithSAO() { if (!this._colorRendererWithSAO) { this._colorRendererWithSAO = new TrianglesColorRenderer(this._scene, true); } return this._colorRendererWithSAO; } get flatColorRenderer() { if (!this._flatColorRenderer) { this._flatColorRenderer = new TrianglesFlatColorRenderer(this._scene, false); } return this._flatColorRenderer; } get flatColorRendererWithSAO() { if (!this._flatColorRendererWithSAO) { this._flatColorRendererWithSAO = new TrianglesFlatColorRenderer(this._scene, true); } return this._flatColorRendererWithSAO; } get colorTextureRenderer() { if (!this._colorTextureRenderer) { this._colorTextureRenderer = new TrianglesColorTextureRenderer(this._scene, false); } return this._colorTextureRenderer; } get colorTextureRendererWithSAO() { if (!this._colorTextureRendererWithSAO) { this._colorTextureRendererWithSAO = new TrianglesColorTextureRenderer(this._scene, true); } return this._colorTextureRendererWithSAO; } get colorTextureRendererAlphaCutoff() { if (!this._colorTextureRendererAlphaCutoff) { this._colorTextureRendererAlphaCutoff = new TrianglesColorTextureRenderer(this._scene, false, { useAlphaCutoff: true }); } return this._colorTextureRendererAlphaCutoff; } get colorTextureRendererWithSAOAlphaCutoff() { if (!this._colorTextureRendererWithSAOAlphaCutoff) { this._colorTextureRendererWithSAOAlphaCutoff = new TrianglesColorTextureRenderer(this._scene, true, { useAlphaCutoff: true }); } return this._colorTextureRendererWithSAOAlphaCutoff; } get pbrRenderer() { if (!this._pbrRenderer) { this._pbrRenderer = new TrianglesPBRRenderer(this._scene, false); } return this._pbrRenderer; } get pbrRendererWithSAO() { if (!this._pbrRendererWithSAO) { this._pbrRendererWithSAO = new TrianglesPBRRenderer(this._scene, true); } return this._pbrRendererWithSAO; } get silhouetteRenderer() { if (!this._silhouetteRenderer) { this._silhouetteRenderer = new TrianglesSilhouetteRenderer(this._scene); } return this._silhouetteRenderer; } get depthRenderer() { if (!this._depthRenderer) { this._depthRenderer = new TrianglesDepthRenderer(this._scene); } return this._depthRenderer; } get normalsRenderer() { if (!this._normalsRenderer) { this._normalsRenderer = new TrianglesNormalsRenderer(this._scene); } return this._normalsRenderer; } get edgesRenderer() { if (!this._edgesRenderer) { this._edgesRenderer = new EdgesEmphasisRenderer(this._scene); } return this._edgesRenderer; } get edgesColorRenderer() { if (!this._edgesColorRenderer) { this._edgesColorRenderer = new EdgesColorRenderer(this._scene); } return this._edgesColorRenderer; } get pickMeshRenderer() { if (!this._pickMeshRenderer) { this._pickMeshRenderer = new TrianglesPickMeshRenderer(this._scene); } return this._pickMeshRenderer; } get pickNormalsRenderer() { if (!this._pickNormalsRenderer) { this._pickNormalsRenderer = new TrianglesPickNormalsRenderer(this._scene); } return this._pickNormalsRenderer; } get pickNormalsFlatRenderer() { if (!this._pickNormalsFlatRenderer) { this._pickNormalsFlatRenderer = new TrianglesPickNormalsFlatRenderer(this._scene); } return this._pickNormalsFlatRenderer; } get pickDepthRenderer() { if (!this._pickDepthRenderer) { this._pickDepthRenderer = new TrianglesPickDepthRenderer(this._scene); } return this._pickDepthRenderer; } get occlusionRenderer() { if (!this._occlusionRenderer) { this._occlusionRenderer = new TrianglesOcclusionRenderer(this._scene); } return this._occlusionRenderer; } get shadowRenderer() { if (!this._shadowRenderer) { this._shadowRenderer = new TrianglesShadowRenderer(this._scene); } return this._shadowRenderer; } get snapRenderer() { if (!this._snapRenderer) { this._snapRenderer = new TrianglesSnapRenderer(this._scene); } return this._snapRenderer; } get snapInitRenderer() { if (!this._snapInitRenderer) { this._snapInitRenderer = new TrianglesSnapInitRenderer(this._scene); } return this._snapInitRenderer; } _destroy() { if (this._colorRenderer) { this._colorRenderer.destroy(); } if (this._colorRendererWithSAO) { this._colorRendererWithSAO.destroy(); } if (this._flatColorRenderer) { this._flatColorRenderer.destroy(); } if (this._flatColorRendererWithSAO) { this._flatColorRendererWithSAO.destroy(); } if (this._colorTextureRenderer) { this._colorTextureRenderer.destroy(); } if (this._colorTextureRendererWithSAO) { this._colorTextureRendererWithSAO.destroy(); } if (this._colorTextureRendererAlphaCutoff) { this._colorTextureRendererAlphaCutoff.destroy(); } if (this._colorTextureRendererWithSAOAlphaCutoff) { this._colorTextureRendererWithSAOAlphaCutoff.destroy(); } if (this._pbrRenderer) { this._pbrRenderer.destroy(); } if (this._pbrRendererWithSAO) { this._pbrRendererWithSAO.destroy(); } if (this._depthRenderer) { this._depthRenderer.destroy(); } if (this._normalsRenderer) { this._normalsRenderer.destroy(); } if (this._silhouetteRenderer) { this._silhouetteRenderer.destroy(); } if (this._edgesRenderer) { this._edgesRenderer.destroy(); } if (this._edgesColorRenderer) { this._edgesColorRenderer.destroy(); } if (this._pickMeshRenderer) { this._pickMeshRenderer.destroy(); } if (this._pickDepthRenderer) { this._pickDepthRenderer.destroy(); } if (this._pickNormalsRenderer) { this._pickNormalsRenderer.destroy(); } if (this._pickNormalsFlatRenderer) { this._pickNormalsFlatRenderer.destroy(); } if (this._occlusionRenderer) { this._occlusionRenderer.destroy(); } if (this._shadowRenderer) { this._shadowRenderer.destroy(); } if (this._snapInitRenderer) { this._snapInitRenderer.destroy(); } if (this._snapRenderer) { this._snapRenderer.destroy(); } } } const cachedRenderers$5 = {}; /** * @private */ function getRenderers$6(scene) { const sceneId = scene.id; let renderers = cachedRenderers$5[sceneId]; if (!renderers) { renderers = new Renderers(scene); cachedRenderers$5[sceneId] = renderers; renderers._compile(); renderers.eagerCreateRenders(); scene.on("compile", () => { renderers._compile(); renderers.eagerCreateRenders(); }); scene.on("destroyed", () => { delete cachedRenderers$5[sceneId]; renderers._destroy(); }); } return renderers; } const tempUint8Vec4$2 = new Uint8Array(4); const tempFloat32$2 = new Float32Array(1); const tempVec4a$c = math.vec4([0, 0, 0, 1]); const tempVec3fa$2 = new Float32Array(3); const tempVec3a$z = math.vec3(); const tempVec3b$v = math.vec3(); const tempVec3c$q = math.vec3(); const tempVec3d$9 = math.vec3(); const tempVec3e$1 = math.vec3(); const tempVec3f$1 = math.vec3(); const tempVec3g = math.vec3(); const tempFloat32Vec4$2 = new Float32Array(4); /** * @private */ class VBOInstancingTrianglesLayer { /** * @param cfg * @param cfg.layerIndex * @param cfg.model * @param cfg.geometry * @param cfg.textureSet * @param cfg.origin */ constructor(cfg) { // console.info("Creating VBOInstancingTrianglesLayer"); /** * Owner model * @type {VBOSceneModel} */ this.model = cfg.model; /** * State sorting key. * @type {string} */ this.sortId = "TrianglesInstancingLayer" + (cfg.solid ? "-solid" : "-surface") + (cfg.normals ? "-normals" : "-autoNormals"); /** * Index of this InstancingLayer in VBOSceneModel#_layerList * @type {Number} */ this.layerIndex = cfg.layerIndex; this._renderers = getRenderers$6(cfg.model.scene); this._aabb = math.collapseAABB3(); this._state = new RenderState({ numInstances: 0, obb: math.OBB3(), origin: math.vec3(), geometry: cfg.geometry, textureSet: cfg.textureSet, pbrSupported: false, // Set in #finalize if we have enough to support quality rendering positionsDecodeMatrix: cfg.geometry.positionsDecodeMatrix, // So we can null the geometry for GC colorsBuf: null, metallicRoughnessBuf: null, flagsBuf: null, offsetsBuf: null, modelMatrixBuf: null, modelMatrixCol0Buf: null, modelMatrixCol1Buf: null, modelMatrixCol2Buf: null, modelNormalMatrixCol0Buf: null, modelNormalMatrixCol1Buf: null, modelNormalMatrixCol2Buf: null, pickColorsBuf: null }); // These counts are used to avoid unnecessary render passes this._numPortions = 0; this._numVisibleLayerPortions = 0; this._numTransparentLayerPortions = 0; this._numXRayedLayerPortions = 0; this._numHighlightedLayerPortions = 0; this._numSelectedLayerPortions = 0; this._numClippableLayerPortions = 0; this._numEdgesLayerPortions = 0; this._numPickableLayerPortions = 0; this._numCulledLayerPortions = 0; /** @private */ this.numIndices = cfg.geometry.numIndices; // Vertex arrays this._colors = []; this._metallicRoughness = []; this._pickColors = []; this._offsets = []; // Modeling matrix per instance, array for each column this._modelMatrix = []; this._modelMatrixCol0 = []; this._modelMatrixCol1 = []; this._modelMatrixCol2 = []; // Modeling normal matrix per instance, array for each column this._modelNormalMatrixCol0 = []; this._modelNormalMatrixCol1 = []; this._modelNormalMatrixCol2 = []; this._portions = []; this._meshes = []; this._aabb = math.collapseAABB3(); this.aabbDirty = true; if (cfg.origin) { this._state.origin.set(cfg.origin); } this._finalized = false; /** * When true, this layer contains solid triangle meshes, otherwise this layer contains surface triangle meshes * @type {boolean} */ this.solid = !!cfg.solid; /** * The number of indices in this layer. * @type {number|*} */ this.numIndices = cfg.geometry.numIndices; /** * The type of primitives in this layer. */ this.primitive = cfg.geometry.primitive; } get aabb() { if (this.aabbDirty) { math.collapseAABB3(this._aabb); for (let i = 0, len = this._meshes.length; i < len; i++) { math.expandAABB3(this._aabb, this._meshes[i].aabb); } this.aabbDirty = false; } return this._aabb; } /** * Creates a new portion within this InstancingLayer, returns the new portion ID. * * The portion will instance this InstancingLayer's geometry. * * Gives the portion the specified color and matrix. * * @param mesh The SceneModelMesh that owns the portion * @param cfg Portion params * @param cfg.color Color [0..255,0..255,0..255] * @param cfg.metallic Metalness factor [0..255] * @param cfg.roughness Roughness factor [0..255] * @param cfg.opacity Opacity [0..255]. * @param cfg.meshMatrix Flat float 4x4 matrix. * @param [cfg.worldMatrix] Flat float 4x4 matrix. * @param cfg.pickColor Quantized pick color * @returns {number} Portion ID. */ createPortion(mesh, cfg) { const color = cfg.color; const metallic = cfg.metallic; const roughness = cfg.roughness; const opacity = cfg.opacity !== null && cfg.opacity !== undefined ? cfg.opacity : 255; const meshMatrix = cfg.meshMatrix; const pickColor = cfg.pickColor; if (this._finalized) { throw "Already finalized"; } const r = color[0]; // Color is pre-quantized by SceneModel const g = color[1]; const b = color[2]; this._colors.push(r); this._colors.push(g); this._colors.push(b); this._colors.push(opacity); this._metallicRoughness.push((metallic !== null && metallic !== undefined) ? metallic : 0); this._metallicRoughness.push((roughness !== null && roughness !== undefined) ? roughness : 255); if (this.model.scene.entityOffsetsEnabled) { this._offsets.push(0); this._offsets.push(0); this._offsets.push(0); } this._modelMatrixCol0.push(meshMatrix[0]); this._modelMatrixCol0.push(meshMatrix[4]); this._modelMatrixCol0.push(meshMatrix[8]); this._modelMatrixCol0.push(meshMatrix[12]); this._modelMatrixCol1.push(meshMatrix[1]); this._modelMatrixCol1.push(meshMatrix[5]); this._modelMatrixCol1.push(meshMatrix[9]); this._modelMatrixCol1.push(meshMatrix[13]); this._modelMatrixCol2.push(meshMatrix[2]); this._modelMatrixCol2.push(meshMatrix[6]); this._modelMatrixCol2.push(meshMatrix[10]); this._modelMatrixCol2.push(meshMatrix[14]); if (this._state.geometry.normals) { // Note: order of inverse and transpose doesn't matter let transposedMat = math.transposeMat4(meshMatrix, math.mat4()); // TODO: Use cached matrix let normalMatrix = math.inverseMat4(transposedMat); this._modelNormalMatrixCol0.push(normalMatrix[0]); this._modelNormalMatrixCol0.push(normalMatrix[4]); this._modelNormalMatrixCol0.push(normalMatrix[8]); this._modelNormalMatrixCol0.push(normalMatrix[12]); this._modelNormalMatrixCol1.push(normalMatrix[1]); this._modelNormalMatrixCol1.push(normalMatrix[5]); this._modelNormalMatrixCol1.push(normalMatrix[9]); this._modelNormalMatrixCol1.push(normalMatrix[13]); this._modelNormalMatrixCol2.push(normalMatrix[2]); this._modelNormalMatrixCol2.push(normalMatrix[6]); this._modelNormalMatrixCol2.push(normalMatrix[10]); this._modelNormalMatrixCol2.push(normalMatrix[14]); } // Per-vertex pick colors this._pickColors.push(pickColor[0]); this._pickColors.push(pickColor[1]); this._pickColors.push(pickColor[2]); this._pickColors.push(pickColor[3]); this._state.numInstances++; const portionId = this._portions.length; const portion = {}; if (this.model.scene.readableGeometryEnabled) { portion.matrix = meshMatrix.slice(); portion.inverseMatrix = null; // Lazy-computed in precisionRayPickSurface portion.normalMatrix = null; // Lazy-computed in precisionRayPickSurface } this._portions.push(portion); this._numPortions++; this.model.numPortions++; this._meshes.push(mesh); return portionId; } finalize() { if (this._finalized) { return; } const state = this._state; const geometry = state.geometry; const textureSet = state.textureSet; const gl = this.model.scene.canvas.gl; const colorsLength = this._colors.length; const flagsLength = colorsLength / 4; if (colorsLength > 0) { let notNormalized = false; state.colorsBuf = new ArrayBuf(gl, gl.ARRAY_BUFFER, new Uint8Array(this._colors), this._colors.length, 4, gl.DYNAMIC_DRAW, notNormalized); this._colors = []; // Release memory } if (this._metallicRoughness.length > 0) { const metallicRoughness = new Uint8Array(this._metallicRoughness); let normalized = false; state.metallicRoughnessBuf = new ArrayBuf(gl, gl.ARRAY_BUFFER, metallicRoughness, this._metallicRoughness.length, 2, gl.STATIC_DRAW, normalized); } if (flagsLength > 0) { // Because we only build flags arrays here, // get their length from the colors array let notNormalized = false; state.flagsBuf = new ArrayBuf(gl, gl.ARRAY_BUFFER, new Float32Array(flagsLength), flagsLength, 1, gl.DYNAMIC_DRAW, notNormalized); } if (this.model.scene.entityOffsetsEnabled) { if (this._offsets.length > 0) { const notNormalized = false; state.offsetsBuf = new ArrayBuf(gl, gl.ARRAY_BUFFER, new Float32Array(this._offsets), this._offsets.length, 3, gl.DYNAMIC_DRAW, notNormalized); this._offsets = []; // Release memory } } if (geometry.positionsCompressed && geometry.positionsCompressed.length > 0) { const normalized = false; state.positionsBuf = new ArrayBuf(gl, gl.ARRAY_BUFFER, geometry.positionsCompressed, geometry.positionsCompressed.length, 3, gl.STATIC_DRAW, normalized); state.positionsDecodeMatrix = math.mat4(geometry.positionsDecodeMatrix); } // if (geometry.normalsCompressed && geometry.normalsCompressed.length > 0) { // const normalized = true; // For oct-encoded UInt8 // state.normalsBuf = new ArrayBuf(gl, gl.ARRAY_BUFFER, geometry.normalsCompressed, geometry.normalsCompressed.length, 3, gl.STATIC_DRAW, normalized); // } if (geometry.colorsCompressed && geometry.colorsCompressed.length > 0) { const colorsCompressed = new Uint8Array(geometry.colorsCompressed); const notNormalized = false; state.colorsBuf = new ArrayBuf(gl, gl.ARRAY_BUFFER, colorsCompressed, colorsCompressed.length, 4, gl.STATIC_DRAW, notNormalized); } if (geometry.uvCompressed && geometry.uvCompressed.length > 0) { const uvCompressed = geometry.uvCompressed; state.uvDecodeMatrix = geometry.uvDecodeMatrix; state.uvBuf = new ArrayBuf(gl, gl.ARRAY_BUFFER, uvCompressed, uvCompressed.length, 2, gl.STATIC_DRAW, false); } if (geometry.indices && geometry.indices.length > 0) { state.indicesBuf = new ArrayBuf(gl, gl.ELEMENT_ARRAY_BUFFER, new Uint32Array(geometry.indices), geometry.indices.length, 1, gl.STATIC_DRAW); state.numIndices = geometry.indices.length; } if (geometry.edgeIndices && geometry.edgeIndices.length > 0) { state.edgeIndicesBuf = new ArrayBuf(gl, gl.ELEMENT_ARRAY_BUFFER, new Uint32Array(geometry.edgeIndices), geometry.edgeIndices.length, 1, gl.STATIC_DRAW); } if (this._modelMatrixCol0.length > 0) { const normalized = false; state.modelMatrixCol0Buf = new ArrayBuf(gl, gl.ARRAY_BUFFER, new Float32Array(this._modelMatrixCol0), this._modelMatrixCol0.length, 4, gl.STATIC_DRAW, normalized); state.modelMatrixCol1Buf = new ArrayBuf(gl, gl.ARRAY_BUFFER, new Float32Array(this._modelMatrixCol1), this._modelMatrixCol1.length, 4, gl.STATIC_DRAW, normalized); state.modelMatrixCol2Buf = new ArrayBuf(gl, gl.ARRAY_BUFFER, new Float32Array(this._modelMatrixCol2), this._modelMatrixCol2.length, 4, gl.STATIC_DRAW, normalized); this._modelMatrixCol0 = []; this._modelMatrixCol1 = []; this._modelMatrixCol2 = []; if (state.normalsBuf) { state.modelNormalMatrixCol0Buf = new ArrayBuf(gl, gl.ARRAY_BUFFER, new Float32Array(this._modelNormalMatrixCol0), this._modelNormalMatrixCol0.length, 4, gl.STATIC_DRAW, normalized); state.modelNormalMatrixCol1Buf = new ArrayBuf(gl, gl.ARRAY_BUFFER, new Float32Array(this._modelNormalMatrixCol1), this._modelNormalMatrixCol1.length, 4, gl.STATIC_DRAW, normalized); state.modelNormalMatrixCol2Buf = new ArrayBuf(gl, gl.ARRAY_BUFFER, new Float32Array(this._modelNormalMatrixCol2), this._modelNormalMatrixCol2.length, 4, gl.STATIC_DRAW, normalized); this._modelNormalMatrixCol0 = []; this._modelNormalMatrixCol1 = []; this._modelNormalMatrixCol2 = []; } } if (this._pickColors.length > 0) { const normalized = false; state.pickColorsBuf = new ArrayBuf(gl, gl.ARRAY_BUFFER, new Uint8Array(this._pickColors), this._pickColors.length, 4, gl.STATIC_DRAW, normalized); this._pickColors = []; // Release memory } state.pbrSupported = !!state.metallicRoughnessBuf && !!state.uvBuf && !!state.normalsBuf && !!textureSet && !!textureSet.colorTexture && !!textureSet.metallicRoughnessTexture; state.colorTextureSupported = !!state.uvBuf && !!textureSet && !!textureSet.colorTexture; if (!this.model.scene.readableGeometryEnabled) { this._state.geometry = null; } this._finalized = true; } // The following setters are called by VBOSceneModelMesh, in turn called by VBOSceneModelNode, only after the layer is finalized. // It's important that these are called after finalize() in order to maintain integrity of counts like _numVisibleLayerPortions etc. initFlags(portionId, flags, meshTransparent) { if (flags & ENTITY_FLAGS.VISIBLE) { this._numVisibleLayerPortions++; this.model.numVisibleLayerPortions++; } if (flags & ENTITY_FLAGS.HIGHLIGHTED) { this._numHighlightedLayerPortions++; this.model.numHighlightedLayerPortions++; } if (flags & ENTITY_FLAGS.XRAYED) { this._numXRayedLayerPortions++; this.model.numXRayedLayerPortions++; } if (flags & ENTITY_FLAGS.SELECTED) { this._numSelectedLayerPortions++; this.model.numSelectedLayerPortions++; } if (flags & ENTITY_FLAGS.CLIPPABLE) { this._numClippableLayerPortions++; this.model.numClippableLayerPortions++; } if (flags & ENTITY_FLAGS.EDGES) { this._numEdgesLayerPortions++; this.model.numEdgesLayerPortions++; } if (flags & ENTITY_FLAGS.PICKABLE) { this._numPickableLayerPortions++; this.model.numPickableLayerPortions++; } if (flags & ENTITY_FLAGS.CULLED) { this._numCulledLayerPortions++; this.model.numCulledLayerPortions++; } if (meshTransparent) { this._numTransparentLayerPortions++; this.model.numTransparentLayerPortions++; } this._setFlags(portionId, flags, meshTransparent); } setVisible(portionId, flags, meshTransparent) { if (!this._finalized) { throw "Not finalized"; } if (flags & ENTITY_FLAGS.VISIBLE) { this._numVisibleLayerPortions++; this.model.numVisibleLayerPortions++; } else { this._numVisibleLayerPortions--; this.model.numVisibleLayerPortions--; } this._setFlags(portionId, flags, meshTransparent); } setHighlighted(portionId, flags, meshTransparent) { if (!this._finalized) { throw "Not finalized"; } if (flags & ENTITY_FLAGS.HIGHLIGHTED) { this._numHighlightedLayerPortions++; this.model.numHighlightedLayerPortions++; } else { this._numHighlightedLayerPortions--; this.model.numHighlightedLayerPortions--; } this._setFlags(portionId, flags, meshTransparent); } setXRayed(portionId, flags, meshTransparent) { if (!this._finalized) { throw "Not finalized"; } if (flags & ENTITY_FLAGS.XRAYED) { this._numXRayedLayerPortions++; this.model.numXRayedLayerPortions++; } else { this._numXRayedLayerPortions--; this.model.numXRayedLayerPortions--; } this._setFlags(portionId, flags, meshTransparent); } setSelected(portionId, flags, meshTransparent) { if (!this._finalized) { throw "Not finalized"; } if (flags & ENTITY_FLAGS.SELECTED) { this._numSelectedLayerPortions++; this.model.numSelectedLayerPortions++; } else { this._numSelectedLayerPortions--; this.model.numSelectedLayerPortions--; } this._setFlags(portionId, flags, meshTransparent); } setEdges(portionId, flags, meshTransparent) { if (!this._finalized) { throw "Not finalized"; } if (flags & ENTITY_FLAGS.EDGES) { this._numEdgesLayerPortions++; this.model.numEdgesLayerPortions++; } else { this._numEdgesLayerPortions--; this.model.numEdgesLayerPortions--; } this._setFlags(portionId, flags, meshTransparent); } setClippable(portionId, flags) { if (!this._finalized) { throw "Not finalized"; } if (flags & ENTITY_FLAGS.CLIPPABLE) { this._numClippableLayerPortions++; this.model.numClippableLayerPortions++; } else { this._numClippableLayerPortions--; this.model.numClippableLayerPortions--; } this._setFlags(portionId, flags); } setCollidable(portionId, flags) { if (!this._finalized) { throw "Not finalized"; } } setPickable(portionId, flags, meshTransparent) { if (!this._finalized) { throw "Not finalized"; } if (flags & ENTITY_FLAGS.PICKABLE) { this._numPickableLayerPortions++; this.model.numPickableLayerPortions++; } else { this._numPickableLayerPortions--; this.model.numPickableLayerPortions--; } this._setFlags(portionId, flags, meshTransparent); } setCulled(portionId, flags, meshTransparent) { if (!this._finalized) { throw "Not finalized"; } if (flags & ENTITY_FLAGS.CULLED) { this._numCulledLayerPortions++; this.model.numCulledLayerPortions++; } else { this._numCulledLayerPortions--; this.model.numCulledLayerPortions--; } this._setFlags(portionId, flags, meshTransparent); } setColor(portionId, color) { // RGBA color is normalized as ints if (!this._finalized) { throw "Not finalized"; } tempUint8Vec4$2[0] = color[0]; tempUint8Vec4$2[1] = color[1]; tempUint8Vec4$2[2] = color[2]; tempUint8Vec4$2[3] = color[3]; if (this._state.colorsBuf) { this._state.colorsBuf.setData(tempUint8Vec4$2, portionId * 4); } } setTransparent(portionId, flags, transparent) { if (transparent) { this._numTransparentLayerPortions++; this.model.numTransparentLayerPortions++; } else { this._numTransparentLayerPortions--; this.model.numTransparentLayerPortions--; } this._setFlags(portionId, flags, transparent); } // setMatrix(portionId, matrix) { // // if (!this._finalized) { // throw "Not finalized"; // } // // var offset = portionId * 4; // // tempFloat32Vec4[0] = matrix[0]; // tempFloat32Vec4[1] = matrix[4]; // tempFloat32Vec4[2] = matrix[8]; // tempFloat32Vec4[3] = matrix[12]; // // this._state.modelMatrixCol0Buf.setData(tempFloat32Vec4, offset); // // tempFloat32Vec4[0] = matrix[1]; // tempFloat32Vec4[1] = matrix[5]; // tempFloat32Vec4[2] = matrix[9]; // tempFloat32Vec4[3] = matrix[13]; // // this._state.modelMatrixCol1Buf.setData(tempFloat32Vec4, offset); // // tempFloat32Vec4[0] = matrix[2]; // tempFloat32Vec4[1] = matrix[6]; // tempFloat32Vec4[2] = matrix[10]; // tempFloat32Vec4[3] = matrix[14]; // // this._state.modelMatrixCol2Buf.setData(tempFloat32Vec4, offset); // } /** * flags are 4bits values encoded on a 32bit base. color flag on the first 4 bits, silhouette flag on the next 4 bits and so on for edge, pick and clippable. */ _setFlags(portionId, flags, meshTransparent) { if (!this._finalized) { throw "Not finalized"; } const visible = !!(flags & ENTITY_FLAGS.VISIBLE); const xrayed = !!(flags & ENTITY_FLAGS.XRAYED); const highlighted = !!(flags & ENTITY_FLAGS.HIGHLIGHTED); const selected = !!(flags & ENTITY_FLAGS.SELECTED); const edges = !!(flags & ENTITY_FLAGS.EDGES); const pickable = !!(flags & ENTITY_FLAGS.PICKABLE); const culled = !!(flags & ENTITY_FLAGS.CULLED); let colorFlag; if (!visible || culled || xrayed || (highlighted && !this.model.scene.highlightMaterial.glowThrough) || (selected && !this.model.scene.selectedMaterial.glowThrough)) { colorFlag = RENDER_PASSES.NOT_RENDERED; } else { if (meshTransparent) { colorFlag = RENDER_PASSES.COLOR_TRANSPARENT; } else { colorFlag = RENDER_PASSES.COLOR_OPAQUE; } } let silhouetteFlag; if (!visible || culled) { silhouetteFlag = RENDER_PASSES.NOT_RENDERED; } else if (selected) { silhouetteFlag = RENDER_PASSES.SILHOUETTE_SELECTED; } else if (highlighted) { silhouetteFlag = RENDER_PASSES.SILHOUETTE_HIGHLIGHTED; } else if (xrayed) { silhouetteFlag = RENDER_PASSES.SILHOUETTE_XRAYED; } else { silhouetteFlag = RENDER_PASSES.NOT_RENDERED; } let edgeFlag = 0; if (!visible || culled) { edgeFlag = RENDER_PASSES.NOT_RENDERED; } else if (selected) { edgeFlag = RENDER_PASSES.EDGES_SELECTED; } else if (highlighted) { edgeFlag = RENDER_PASSES.EDGES_HIGHLIGHTED; } else if (xrayed) { edgeFlag = RENDER_PASSES.EDGES_XRAYED; } else if (edges) { if (meshTransparent) { edgeFlag = RENDER_PASSES.EDGES_COLOR_TRANSPARENT; } else { edgeFlag = RENDER_PASSES.EDGES_COLOR_OPAQUE; } } else { edgeFlag = RENDER_PASSES.NOT_RENDERED; } const pickFlag = (visible && !culled && pickable) ? RENDER_PASSES.PICK : RENDER_PASSES.NOT_RENDERED; const clippableFlag = !!(flags & ENTITY_FLAGS.CLIPPABLE) ? 1 : 0; let vertFlag = 0; vertFlag |= colorFlag; vertFlag |= silhouetteFlag << 4; vertFlag |= edgeFlag << 8; vertFlag |= pickFlag << 12; vertFlag |= clippableFlag << 16; tempFloat32$2[0] = vertFlag; if (this._state.flagsBuf) { this._state.flagsBuf.setData(tempFloat32$2, portionId); } } setOffset(portionId, offset) { if (!this._finalized) { throw "Not finalized"; } if (!this.model.scene.entityOffsetsEnabled) { this.model.error("Entity#offset not enabled for this Viewer"); // See Viewer entityOffsetsEnabled return; } tempVec3fa$2[0] = offset[0]; tempVec3fa$2[1] = offset[1]; tempVec3fa$2[2] = offset[2]; if (this._state.offsetsBuf) { this._state.offsetsBuf.setData(tempVec3fa$2, portionId * 3); } } getEachVertex(portionId, callback) { if (!this.model.scene.readableGeometryEnabled) { return false; } const state = this._state; const geometry = state.geometry; const portion = this._portions[portionId]; if (!portion) { this.model.error("portion not found: " + portionId); return; } const positions = geometry.positionsCompressed; const sceneModelMatrix = this.model.matrix; const origin = math.vec4(); origin.set(state.origin, 0); origin[3] = 1; math.mulMat4v4(sceneModelMatrix, origin, origin); const offsetX = origin[0]; const offsetY = origin[1]; const offsetZ = origin[2]; const worldPos = tempVec4a$c; const portionMatrix = portion.matrix; const positionsDecodeMatrix = state.positionsDecodeMatrix; for (let i = 0, len = positions.length; i < len; i += 3) { worldPos[0] = positions[i]; worldPos[1] = positions[i + 1]; worldPos[2] = positions[i + 2]; math.decompressPosition(worldPos, positionsDecodeMatrix); math.transformPoint3(portionMatrix, worldPos, worldPos); worldPos[3] = 1; math.mulMat4v4(sceneModelMatrix, worldPos, worldPos); worldPos[0] += offsetX; worldPos[1] += offsetY; worldPos[2] += offsetZ; callback(worldPos); } } getEachIndex(portionId, callback) { if (!this.model.scene.readableGeometryEnabled) { return false; } const state = this._state; const geometry = state.geometry; const portion = this._portions[portionId]; if (!portion) { this.model.error("portion not found: " + portionId); return; } const indices = geometry.indices; for (let i = 0, len = indices.length; i < len; i++) { callback(indices[i]); } } setMatrix(portionId, matrix) { if (!this._finalized) { throw "Not finalized"; } //////////////////////////////////////// // TODO: Update portion matrix //////////////////////////////////////// var offset = portionId * 4; tempFloat32Vec4$2[0] = matrix[0]; tempFloat32Vec4$2[1] = matrix[4]; tempFloat32Vec4$2[2] = matrix[8]; tempFloat32Vec4$2[3] = matrix[12]; this._state.modelMatrixCol0Buf.setData(tempFloat32Vec4$2, offset); tempFloat32Vec4$2[0] = matrix[1]; tempFloat32Vec4$2[1] = matrix[5]; tempFloat32Vec4$2[2] = matrix[9]; tempFloat32Vec4$2[3] = matrix[13]; this._state.modelMatrixCol1Buf.setData(tempFloat32Vec4$2, offset); tempFloat32Vec4$2[0] = matrix[2]; tempFloat32Vec4$2[1] = matrix[6]; tempFloat32Vec4$2[2] = matrix[10]; tempFloat32Vec4$2[3] = matrix[14]; this._state.modelMatrixCol2Buf.setData(tempFloat32Vec4$2, offset); } readGeometryData(portionId) { if (!this._finalized) { throw "Not finalized"; } const state = this._state; const sceneModelMatrix = this.model.matrix; const col0 = state.modelMatrixCol0Buf.getData(portionId, 1); const col1 = state.modelMatrixCol1Buf.getData(portionId, 1); const col2 = state.modelMatrixCol2Buf.getData(portionId, 1); const portionMatrix = [ col0[0], col1[0], col2[0], 0, col0[1], col1[1], col2[1], 0, col0[2], col1[2], col2[2], 0, col0[3], col1[3], col2[3], 1, ]; const positionsDecodeMatrix = state.positionsDecodeMatrix; const origin = math.vec4(); origin.set(state.origin, 0); origin[3] = 1; math.mulMat4v4(sceneModelMatrix, origin, origin); const indices = state.indicesBuf.getData(); const matrix = math.mulMat4( portionMatrix, positionsDecodeMatrix, new Array(16) ); math.mulMat4( sceneModelMatrix, matrix, matrix ); matrix[12] += origin[0]; matrix[13] += origin[1]; matrix[14] += origin[2]; const positionsQuantized = state.positionsBuf.getData(); const positions = math.transformPositions3( matrix, positionsQuantized, new Array(positionsQuantized.length) ); // const aabb = math.positions3ToAABB3(positions); // console.log({aabbToniInstancing: aabb}); return { indices, positions }; } // ---------------------- COLOR RENDERING ----------------------------------- drawColorOpaque(renderFlags, frameCtx) { if (this._numCulledLayerPortions === this._numPortions || this._numVisibleLayerPortions === 0 || this._numTransparentLayerPortions === this._numPortions || this._numXRayedLayerPortions === this._numPortions) { return; } this._updateBackfaceCull(renderFlags, frameCtx); const useAlphaCutoff = this._state.textureSet && (typeof (this._state.textureSet.alphaCutoff) === "number"); if (frameCtx.withSAO && this.model.saoEnabled) { if (frameCtx.pbrEnabled && this.model.pbrEnabled && this._state.pbrSupported) { if (this._renderers.pbrRendererWithSAO) { this._renderers.pbrRendererWithSAO.drawLayer(frameCtx, this, RENDER_PASSES.COLOR_OPAQUE); } } else if (frameCtx.colorTextureEnabled && this.model.colorTextureEnabled && this._state.colorTextureSupported) { if (useAlphaCutoff) { if (this._renderers.colorTextureRendererWithSAOAlphaCutoff) { this._renderers.colorTextureRendererWithSAOAlphaCutoff.drawLayer(frameCtx, this, RENDER_PASSES.COLOR_OPAQUE); } } else { if (this._renderers.colorTextureRendererWithSAO) { this._renderers.colorTextureRendererWithSAO.drawLayer(frameCtx, this, RENDER_PASSES.COLOR_OPAQUE); } } } else if (this._state.normalsBuf) { if (this._renderers.colorRendererWithSAO) { this._renderers.colorRendererWithSAO.drawLayer(frameCtx, this, RENDER_PASSES.COLOR_OPAQUE); } } else { if (this._renderers.flatColorRendererWithSAO) { this._renderers.flatColorRendererWithSAO.drawLayer(frameCtx, this, RENDER_PASSES.COLOR_OPAQUE); } } } else if (frameCtx.pbrEnabled && this.model.pbrEnabled && this._state.pbrSupported) { if (this._renderers.pbrRenderer) { this._renderers.pbrRenderer.drawLayer(frameCtx, this, RENDER_PASSES.COLOR_OPAQUE); } } else if (frameCtx.colorTextureEnabled && this.model.colorTextureEnabled && this._state.colorTextureSupported) { if (useAlphaCutoff) { if (this._renderers.colorTextureRendererAlphaCutoff) { this._renderers.colorTextureRendererAlphaCutoff.drawLayer(frameCtx, this, RENDER_PASSES.COLOR_OPAQUE); } } else { if (this._renderers.colorTextureRenderer) { this._renderers.colorTextureRenderer.drawLayer(frameCtx, this, RENDER_PASSES.COLOR_OPAQUE); } } } else if (this._state.normalsBuf) { if (this._renderers.colorRenderer) { this._renderers.colorRenderer.drawLayer(frameCtx, this, RENDER_PASSES.COLOR_OPAQUE); } } else { if (this._renderers.flatColorRenderer) { this._renderers.flatColorRenderer.drawLayer(frameCtx, this, RENDER_PASSES.COLOR_OPAQUE); } } } _updateBackfaceCull(renderFlags, frameCtx) { const backfaces = true; // See XCD-230 if (frameCtx.backfaces !== backfaces) { const gl = frameCtx.gl; { gl.disable(gl.CULL_FACE); } frameCtx.backfaces = backfaces; } } drawColorTransparent(renderFlags, frameCtx) { if (this._numCulledLayerPortions === this._numPortions || this._numVisibleLayerPortions === 0 || this._numTransparentLayerPortions === 0 || this._numXRayedLayerPortions === this._numPortions) { return; } this._updateBackfaceCull(renderFlags, frameCtx); if (frameCtx.pbrEnabled && this.model.pbrEnabled && this._state.pbrSupported) { if (this._renderers.pbrRenderer) { this._renderers.pbrRenderer.drawLayer(frameCtx, this, RENDER_PASSES.COLOR_TRANSPARENT); } } else if (frameCtx.colorTextureEnabled && this.model.colorTextureEnabled && this._state.colorTextureSupported) { if (this._renderers.colorTextureRenderer) { this._renderers.colorTextureRenderer.drawLayer(frameCtx, this, RENDER_PASSES.COLOR_TRANSPARENT); } } else if (this._state.normalsBuf) { if (this._renderers.colorRenderer) { this._renderers.colorRenderer.drawLayer(frameCtx, this, RENDER_PASSES.COLOR_TRANSPARENT); } } else { if (this._renderers.flatColorRenderer) { this._renderers.flatColorRenderer.drawLayer(frameCtx, this, RENDER_PASSES.COLOR_TRANSPARENT); } } } // ---------------------- RENDERING SAO POST EFFECT TARGETS -------------- drawDepth(renderFlags, frameCtx) { if (this._numCulledLayerPortions === this._numPortions || this._numVisibleLayerPortions === 0 || this._numTransparentLayerPortions === this._numPortions || this._numXRayedLayerPortions === this._numPortions) { return; } this._updateBackfaceCull(renderFlags, frameCtx); if (this._renderers.depthRenderer) { this._renderers.depthRenderer.drawLayer(frameCtx, this, RENDER_PASSES.COLOR_OPAQUE); // Assume whatever post-effect uses depth (eg SAO) does not apply to transparent objects } } drawNormals(renderFlags, frameCtx) { if (this._numCulledLayerPortions === this._numPortions || this._numVisibleLayerPortions === 0 || this._numTransparentLayerPortions === this._numPortions || this._numXRayedLayerPortions === this._numPortions) { return; } this._updateBackfaceCull(renderFlags, frameCtx); if (this._renderers.normalsRenderer) { this._renderers.normalsRenderer.drawLayer(frameCtx, this, RENDER_PASSES.COLOR_OPAQUE); // Assume whatever post-effect uses normals (eg SAO) does not apply to transparent objects } } // ---------------------- SILHOUETTE RENDERING ----------------------------------- drawSilhouetteXRayed(renderFlags, frameCtx) { if (this._numCulledLayerPortions === this._numPortions || this._numVisibleLayerPortions === 0 || this._numXRayedLayerPortions === 0) { return; } this._updateBackfaceCull(renderFlags, frameCtx); if (this._renderers.silhouetteRenderer) { this._renderers.silhouetteRenderer.drawLayer(frameCtx, this, RENDER_PASSES.SILHOUETTE_XRAYED); } } drawSilhouetteHighlighted(renderFlags, frameCtx) { if (this._numCulledLayerPortions === this._numPortions || this._numVisibleLayerPortions === 0 || this._numHighlightedLayerPortions === 0) { return; } this._updateBackfaceCull(renderFlags, frameCtx); if (this._renderers.silhouetteRenderer) { this._renderers.silhouetteRenderer.drawLayer(frameCtx, this, RENDER_PASSES.SILHOUETTE_HIGHLIGHTED); } } drawSilhouetteSelected(renderFlags, frameCtx) { if (this._numCulledLayerPortions === this._numPortions || this._numVisibleLayerPortions === 0 || this._numSelectedLayerPortions === 0) { return; } this._updateBackfaceCull(renderFlags, frameCtx); if (this._renderers.silhouetteRenderer) { this._renderers.silhouetteRenderer.drawLayer(frameCtx, this, RENDER_PASSES.SILHOUETTE_SELECTED); } } // ---------------------- EDGES RENDERING ----------------------------------- drawEdgesColorOpaque(renderFlags, frameCtx) { if (this._numCulledLayerPortions === this._numPortions || this._numVisibleLayerPortions === 0 || this._numEdgesLayerPortions === 0) { return; } if (this._renderers.edgesColorRenderer) { this._renderers.edgesColorRenderer.drawLayer(frameCtx, this, RENDER_PASSES.EDGES_COLOR_OPAQUE); } } drawEdgesColorTransparent(renderFlags, frameCtx) { if (this._numCulledLayerPortions === this._numPortions || this._numVisibleLayerPortions === 0 || this._numEdgesLayerPortions === 0) { return; } if (this._renderers.edgesColorRenderer) { this._renderers.edgesColorRenderer.drawLayer(frameCtx, this, RENDER_PASSES.EDGES_COLOR_TRANSPARENT); } } drawEdgesXRayed(renderFlags, frameCtx) { if (this._numCulledLayerPortions === this._numPortions || this._numVisibleLayerPortions === 0 || this._numXRayedLayerPortions === 0) { return; } if (this._renderers.edgesRenderer) { this._renderers.edgesRenderer.drawLayer(frameCtx, this, RENDER_PASSES.EDGES_XRAYED); } } drawEdgesHighlighted(renderFlags, frameCtx) { if (this._numCulledLayerPortions === this._numPortions || this._numVisibleLayerPortions === 0 || this._numHighlightedLayerPortions === 0) { return; } if (this._renderers.edgesRenderer) { this._renderers.edgesRenderer.drawLayer(frameCtx, this, RENDER_PASSES.EDGES_HIGHLIGHTED); } } drawEdgesSelected(renderFlags, frameCtx) { if (this._numCulledLayerPortions === this._numPortions || this._numVisibleLayerPortions === 0 || this._numSelectedLayerPortions === 0) { return; } if (this._renderers.edgesRenderer) { this._renderers.edgesRenderer.drawLayer(frameCtx, this, RENDER_PASSES.EDGES_SELECTED); } } // ---------------------- OCCLUSION CULL RENDERING ----------------------------------- drawOcclusion(renderFlags, frameCtx) { if (this._numCulledLayerPortions === this._numPortions || this._numVisibleLayerPortions === 0) { return; } this._updateBackfaceCull(renderFlags, frameCtx); if (this._renderers.occlusionRenderer) { // Only opaque, filled objects can be occluders this._renderers.occlusionRenderer.drawLayer(frameCtx, this, RENDER_PASSES.COLOR_OPAQUE); } } // ---------------------- SHADOW BUFFER RENDERING ----------------------------------- drawShadow(renderFlags, frameCtx) { if (this._numCulledLayerPortions === this._numPortions || this._numVisibleLayerPortions === 0) { return; } this._updateBackfaceCull(renderFlags, frameCtx); if (this._renderers.shadowRenderer) { this._renderers.shadowRenderer.drawLayer(frameCtx, this, RENDER_PASSES.COLOR_OPAQUE); } } //---- PICKING ---------------------------------------------------------------------------------------------------- drawPickMesh(renderFlags, frameCtx) { if (this._numCulledLayerPortions === this._numPortions || this._numVisibleLayerPortions === 0) { return; } this._updateBackfaceCull(renderFlags, frameCtx); if (this._renderers.pickMeshRenderer) { this._renderers.pickMeshRenderer.drawLayer(frameCtx, this, RENDER_PASSES.PICK); } } drawPickDepths(renderFlags, frameCtx) { if (this._numCulledLayerPortions === this._numPortions || this._numVisibleLayerPortions === 0) { return; } this._updateBackfaceCull(renderFlags, frameCtx); if (this._renderers.pickDepthRenderer) { this._renderers.pickDepthRenderer.drawLayer(frameCtx, this, RENDER_PASSES.PICK); } } drawPickNormals(renderFlags, frameCtx) { if (this._numCulledLayerPortions === this._numPortions || this._numVisibleLayerPortions === 0) { return; } this._updateBackfaceCull(renderFlags, frameCtx); //////////////////////////////////////////////////////////////////////////////////////////////////// // TODO // if (this._state.normalsBuf) { // if (this._renderers.pickNormalsRenderer) { // this._renderers.pickNormalsRenderer.drawLayer(frameCtx, this, RENDER_PASSES.PICK); // } //////////////////////////////////////////////////////////////////////////////////////////////////// // } else { if (this._renderers.pickNormalsFlatRenderer) { this._renderers.pickNormalsFlatRenderer.drawLayer(frameCtx, this, RENDER_PASSES.PICK); } // } } drawSnapInit(renderFlags, frameCtx) { if (this._numCulledLayerPortions === this._numPortions || this._numVisibleLayerPortions === 0) { return; } this._updateBackfaceCull(renderFlags, frameCtx); if (this._renderers.snapInitRenderer) { this._renderers.snapInitRenderer.drawLayer(frameCtx, this, RENDER_PASSES.PICK); } } drawSnap(renderFlags, frameCtx) { if (this._numCulledLayerPortions === this._numPortions || this._numVisibleLayerPortions === 0) { return; } this._updateBackfaceCull(renderFlags, frameCtx); if (this._renderers.snapRenderer) { this._renderers.snapRenderer.drawLayer(frameCtx, this, RENDER_PASSES.PICK); } } //----------------------------------------------------------------------------------------- precisionRayPickSurface(portionId, worldRayOrigin, worldRayDir, worldSurfacePos, worldNormal) { if (!this.model.scene.readableGeometryEnabled) { return false; } const geometry = this._state.geometry; const state = this._state; const portion = this._portions[portionId]; if (!portion) { this.model.error("portion not found: " + portionId); return false; } if (!portion.inverseMatrix) { portion.inverseMatrix = math.inverseMat4(portion.matrix, math.mat4()); } if (worldNormal && !portion.normalMatrix) { portion.normalMatrix = math.transposeMat4(portion.inverseMatrix, math.mat4()); } const quantizedPositions = geometry.quantizedPositions; const indices = geometry.indices; const origin = state.origin; const offset = portion.offset; const rtcRayOrigin = tempVec3a$z; const rtcRayDir = tempVec3b$v; rtcRayOrigin.set(origin ? math.subVec3(worldRayOrigin, origin, tempVec3c$q) : worldRayOrigin); // World -> RTC rtcRayDir.set(worldRayDir); if (offset) { math.subVec3(rtcRayOrigin, offset); } math.transformRay(this.model.worldNormalMatrix, rtcRayOrigin, rtcRayDir, rtcRayOrigin, rtcRayDir); math.transformRay(portion.inverseMatrix, rtcRayOrigin, rtcRayDir, rtcRayOrigin, rtcRayDir); const a = tempVec3d$9; const b = tempVec3e$1; const c = tempVec3f$1; let gotIntersect = false; let closestDist = 0; const closestIntersectPos = tempVec3g; for (let i = 0, len = indices.length; i < len; i += 3) { const ia = indices[i + 0] * 3; const ib = indices[i + 1] * 3; const ic = indices[i + 2] * 3; a[0] = quantizedPositions[ia]; a[1] = quantizedPositions[ia + 1]; a[2] = quantizedPositions[ia + 2]; b[0] = quantizedPositions[ib]; b[1] = quantizedPositions[ib + 1]; b[2] = quantizedPositions[ib + 2]; c[0] = quantizedPositions[ic]; c[1] = quantizedPositions[ic + 1]; c[2] = quantizedPositions[ic + 2]; const {positionsDecodeMatrix} = state.geometry; math.decompressPosition(a, positionsDecodeMatrix); math.decompressPosition(b, positionsDecodeMatrix); math.decompressPosition(c, positionsDecodeMatrix); if (math.rayTriangleIntersect(rtcRayOrigin, rtcRayDir, a, b, c, closestIntersectPos)) { math.transformPoint3(portion.matrix, closestIntersectPos, closestIntersectPos); math.transformPoint3(this.model.worldMatrix, closestIntersectPos, closestIntersectPos); if (offset) { math.addVec3(closestIntersectPos, offset); } if (origin) { math.addVec3(closestIntersectPos, origin); } const dist = Math.abs(math.lenVec3(math.subVec3(closestIntersectPos, worldRayOrigin, []))); if (!gotIntersect || dist > closestDist) { closestDist = dist; worldSurfacePos.set(closestIntersectPos); if (worldNormal) { // Not that wasteful to eagerly compute - unlikely to hit >2 surfaces on most geometry math.triangleNormal(a, b, c, worldNormal); } gotIntersect = true; } } } if (gotIntersect && worldNormal) { math.transformVec3(portion.normalMatrix, worldNormal, worldNormal); math.transformVec3(this.model.worldNormalMatrix, worldNormal, worldNormal); math.normalizeVec3(worldNormal); } return gotIntersect; } destroy() { const state = this._state; if (state.colorsBuf) { state.colorsBuf.destroy(); state.colorsBuf = null; } if (state.metallicRoughnessBuf) { state.metallicRoughnessBuf.destroy(); state.metallicRoughnessBuf = null; } if (state.flagsBuf) { state.flagsBuf.destroy(); state.flagsBuf = null; } if (state.offsetsBuf) { state.offsetsBuf.destroy(); state.offsetsBuf = null; } if (state.modelMatrixCol0Buf) { state.modelMatrixCol0Buf.destroy(); state.modelMatrixCol0Buf = null; } if (state.modelMatrixCol1Buf) { state.modelMatrixCol1Buf.destroy(); state.modelMatrixCol1Buf = null; } if (state.modelMatrixCol2Buf) { state.modelMatrixCol2Buf.destroy(); state.modelMatrixCol2Buf = null; } if (state.modelNormalMatrixCol0Buf) { state.modelNormalMatrixCol0Buf.destroy(); state.modelNormalMatrixCol0Buf = null; } if (state.modelNormalMatrixCol1Buf) { state.modelNormalMatrixCol1Buf.destroy(); state.modelNormalMatrixCol1Buf = null; } if (state.modelNormalMatrixCol2Buf) { state.modelNormalMatrixCol2Buf.destroy(); state.modelNormalMatrixCol2Buf = null; } if (state.pickColorsBuf) { state.pickColorsBuf.destroy(); state.pickColorsBuf = null; } state.destroy(); this._state = null; } } /** * @private */ class VBOSceneModelLineBatchingRenderer extends VBORenderer { _draw(drawCfg) { const {gl} = this._scene.canvas; const { state, frameCtx, incrementDrawState } = drawCfg; gl.drawElements(gl.LINES, state.indicesBuf.numItems, state.indicesBuf.itemType, 0); if (incrementDrawState) { frameCtx.drawElements++; } } } /** * @private */ class VBOBatchingLinesColorRenderer extends VBOSceneModelLineBatchingRenderer { drawLayer(frameCtx, layer, renderPass) { super.drawLayer(frameCtx, layer, renderPass, { incrementDrawState: true }); } _buildVertexShader() { const scene = this._scene; const sectionPlanesState = scene._sectionPlanesState; const clipping = sectionPlanesState.getNumAllocatedSectionPlanes() > 0; const src = []; src.push('#version 300 es'); src.push("// Lines batching color vertex shader"); src.push("uniform int renderPass;"); src.push("in vec3 position;"); src.push("in vec4 color;"); src.push("in float flags;"); if (scene.entityOffsetsEnabled) { src.push("in vec3 offset;"); } this._addMatricesUniformBlockLines(src); if (scene.logarithmicDepthBufferEnabled) { src.push("uniform float logDepthBufFC;"); src.push("out float vFragDepth;"); } if (clipping) { src.push("out vec4 vWorldPosition;"); src.push("out float vFlags;"); } src.push("out vec4 vColor;"); src.push("void main(void) {"); // colorFlag = NOT_RENDERED | COLOR_OPAQUE | COLOR_TRANSPARENT // renderPass = COLOR_OPAQUE src.push(`int colorFlag = int(flags) & 0xF;`); src.push(`if (colorFlag != renderPass) {`); src.push(" gl_Position = vec4(0.0, 0.0, 0.0, 0.0);"); // Cull vertex src.push("} else {"); src.push("vec4 worldPosition = worldMatrix * (positionsDecodeMatrix * vec4(position, 1.0)); "); if (scene.entityOffsetsEnabled) { src.push("worldPosition.xyz = worldPosition.xyz + offset;"); } src.push("vec4 viewPosition = viewMatrix * worldPosition; "); src.push("vColor = vec4(float(color.r) / 255.0, float(color.g) / 255.0, float(color.b) / 255.0, float(color.a) / 255.0);"); if (clipping) { src.push("vWorldPosition = worldPosition;"); src.push("vFlags = flags;"); } src.push("vec4 clipPos = projMatrix * viewPosition;"); if (scene.logarithmicDepthBufferEnabled) { src.push("vFragDepth = 1.0 + clipPos.w;"); } src.push("gl_Position = clipPos;"); src.push("}"); src.push("}"); return src; } _buildFragmentShader() { const scene = this._scene; const sectionPlanesState = scene._sectionPlanesState; const clipping = sectionPlanesState.getNumAllocatedSectionPlanes() > 0; const src = []; src.push('#version 300 es'); src.push("// Lines batching color fragment shader"); src.push("#ifdef GL_FRAGMENT_PRECISION_HIGH"); src.push("precision highp float;"); src.push("precision highp int;"); src.push("#else"); src.push("precision mediump float;"); src.push("precision mediump int;"); src.push("#endif"); if (scene.logarithmicDepthBufferEnabled) { src.push("uniform float logDepthBufFC;"); src.push("in float vFragDepth;"); } if (clipping) { src.push("in vec4 vWorldPosition;"); src.push("in float vFlags;"); for (let i = 0, len = sectionPlanesState.getNumAllocatedSectionPlanes(); i < len; i++) { src.push("uniform bool sectionPlaneActive" + i + ";"); src.push("uniform vec3 sectionPlanePos" + i + ";"); src.push("uniform vec3 sectionPlaneDir" + i + ";"); } } src.push("in vec4 vColor;"); src.push("out vec4 outColor;"); src.push("void main(void) {"); if (clipping) { src.push(" bool clippable = (int(vFlags) >> 16 & 0xF) == 1;"); src.push(" if (clippable) {"); src.push(" float dist = 0.0;"); for (let i = 0, len = sectionPlanesState.getNumAllocatedSectionPlanes(); i < len; i++) { src.push("if (sectionPlaneActive" + i + ") {"); src.push(" dist += clamp(dot(-sectionPlaneDir" + i + ".xyz, vWorldPosition.xyz - sectionPlanePos" + i + ".xyz), 0.0, 1000.0);"); src.push("}"); } src.push(" if (dist > 0.0) { discard; }"); src.push("}"); } src.push(" outColor = vColor;"); if (scene.logarithmicDepthBufferEnabled) { src.push("gl_FragDepth = log2( vFragDepth ) * logDepthBufFC * 0.5;"); } src.push("}"); return src; } } /** * @private */ class VBOBatchingLinesSilhouetteRenderer extends VBOSceneModelLineBatchingRenderer { drawLayer(frameCtx, batchingLayer, renderPass) { super.drawLayer(frameCtx, batchingLayer, renderPass, { colorUniform: true }); } _buildVertexShader() { const scene = this._scene; const sectionPlanesState = scene._sectionPlanesState; const clipping = sectionPlanesState.getNumAllocatedSectionPlanes() > 0; const src = []; src.push('#version 300 es'); src.push("// Lines batching silhouette vertex shader"); src.push("uniform int renderPass;"); src.push("in vec3 position;"); if (scene.entityOffsetsEnabled) { src.push("in vec3 offset;"); } src.push("in float flags;"); this._addMatricesUniformBlockLines(src); src.push("uniform vec4 color;"); if (scene.logarithmicDepthBufferEnabled) { src.push("uniform float logDepthBufFC;"); src.push("out float vFragDepth;"); } if (clipping) { src.push("out vec4 vWorldPosition;"); src.push("out float vFlags;"); } src.push("void main(void) {"); // silhouetteFlag = NOT_RENDERED | SILHOUETTE_HIGHLIGHTED | SILHOUETTE_SELECTED | | SILHOUETTE_XRAYED // renderPass = SILHOUETTE_HIGHLIGHTED | SILHOUETTE_SELECTED | | SILHOUETTE_XRAYED src.push(`int silhouetteFlag = int(flags) >> 4 & 0xF;`); src.push(`if (silhouetteFlag != renderPass) {`); src.push(" gl_Position = vec4(0.0, 0.0, 0.0, 0.0);"); // Cull vertex src.push("} else {"); src.push(" vec4 worldPosition = worldMatrix * (positionsDecodeMatrix * vec4(position, 1.0)); "); if (scene.entityOffsetsEnabled) { src.push(" worldPosition.xyz = worldPosition.xyz + offset;"); } src.push("vec4 viewPosition = viewMatrix * worldPosition; "); if (clipping) { src.push("vWorldPosition = worldPosition;"); src.push("vFlags = flags;"); } src.push("vec4 clipPos = projMatrix * viewPosition;"); if (scene.logarithmicDepthBufferEnabled) { src.push("vFragDepth = 1.0 + clipPos.w;"); } src.push("gl_Position = clipPos;"); src.push("}"); src.push("}"); return src; } _buildFragmentShader() { const scene = this._scene; const sectionPlanesState = scene._sectionPlanesState; const clipping = sectionPlanesState.getNumAllocatedSectionPlanes() > 0; const src = []; src.push('#version 300 es'); src.push("// Lines batching silhouette fragment shader"); src.push("#ifdef GL_FRAGMENT_PRECISION_HIGH"); src.push("precision highp float;"); src.push("precision highp int;"); src.push("#else"); src.push("precision mediump float;"); src.push("precision mediump int;"); src.push("#endif"); if (scene.logarithmicDepthBufferEnabled) { src.push("uniform float logDepthBufFC;"); src.push("in float vFragDepth;"); } if (clipping) { src.push("in vec4 vWorldPosition;"); src.push("in float vFlags;"); for (let i = 0, len = sectionPlanesState.getNumAllocatedSectionPlanes(); i < len; i++) { src.push("uniform bool sectionPlaneActive" + i + ";"); src.push("uniform vec3 sectionPlanePos" + i + ";"); src.push("uniform vec3 sectionPlaneDir" + i + ";"); } } src.push("uniform vec4 color;"); src.push("out vec4 outColor;"); src.push("void main(void) {"); if (clipping) { src.push(" bool clippable = (int(vFlags) >> 16 & 0xF) == 1;"); src.push(" if (clippable) {"); src.push(" float dist = 0.0;"); for (let i = 0, len = sectionPlanesState.getNumAllocatedSectionPlanes(); i < len; i++) { src.push("if (sectionPlaneActive" + i + ") {"); src.push(" dist += clamp(dot(-sectionPlaneDir" + i + ".xyz, vWorldPosition.xyz - sectionPlanePos" + i + ".xyz), 0.0, 1000.0);"); src.push("}"); } src.push(" if (dist > 0.0) { discard; }"); src.push("}"); } if (scene.logarithmicDepthBufferEnabled) { src.push("gl_FragDepth = log2( vFragDepth ) * logDepthBufFC * 0.5;"); } src.push("outColor = color;"); src.push("}"); return src; } } const tempVec3a$y = math.vec3(); const tempVec3b$u = math.vec3(); const tempVec3c$p = math.vec3(); const tempVec3d$8 = math.vec3(); const tempMat4a$m = math.mat4(); /** * @private */ class VBOBatchingLinesSnapInitRenderer extends VBORenderer { drawLayer(frameCtx, batchingLayer, renderPass) { if (!this._program) { this._allocate(); if (this.errors) { return; } } if (frameCtx.lastProgramId !== this._program.id) { frameCtx.lastProgramId = this._program.id; this._bindProgram(); } const model = batchingLayer.model; const scene = model.scene; const camera = scene.camera; const gl = scene.canvas.gl; const state = batchingLayer._state; const origin = batchingLayer._state.origin; const {position, rotationMatrix} = model; const aabb = batchingLayer.aabb; // Per-layer AABB for best RTC accuracy const viewMatrix = frameCtx.pickViewMatrix || camera.viewMatrix; if (this._vaoCache.has(batchingLayer)) { gl.bindVertexArray(this._vaoCache.get(batchingLayer)); } else { this._vaoCache.set(batchingLayer, this._makeVAO(state)); } const coordinateScaler = tempVec3a$y; coordinateScaler[0] = math.safeInv(aabb[3] - aabb[0]) * math.MAX_INT; coordinateScaler[1] = math.safeInv(aabb[4] - aabb[1]) * math.MAX_INT; coordinateScaler[2] = math.safeInv(aabb[5] - aabb[2]) * math.MAX_INT; frameCtx.snapPickCoordinateScale[0] = math.safeInv(coordinateScaler[0]); frameCtx.snapPickCoordinateScale[1] = math.safeInv(coordinateScaler[1]); frameCtx.snapPickCoordinateScale[2] = math.safeInv(coordinateScaler[2]); let rtcViewMatrix; let rtcCameraEye; if (origin || position[0] !== 0 || position[1] !== 0 || position[2] !== 0) { const rtcOrigin = tempVec3b$u; if (origin) { const rotatedOrigin = tempVec3c$p; math.transformPoint3(rotationMatrix, origin, rotatedOrigin); rtcOrigin[0] = rotatedOrigin[0]; rtcOrigin[1] = rotatedOrigin[1]; rtcOrigin[2] = rotatedOrigin[2]; } else { rtcOrigin[0] = 0; rtcOrigin[1] = 0; rtcOrigin[2] = 0; } rtcOrigin[0] += position[0]; rtcOrigin[1] += position[1]; rtcOrigin[2] += position[2]; rtcViewMatrix = createRTCViewMat(viewMatrix, rtcOrigin, tempMat4a$m); rtcCameraEye = tempVec3d$8; rtcCameraEye[0] = camera.eye[0] - rtcOrigin[0]; rtcCameraEye[1] = camera.eye[1] - rtcOrigin[1]; rtcCameraEye[2] = camera.eye[2] - rtcOrigin[2]; frameCtx.snapPickOrigin[0] = rtcOrigin[0]; frameCtx.snapPickOrigin[1] = rtcOrigin[1]; frameCtx.snapPickOrigin[2] = rtcOrigin[2]; } else { rtcViewMatrix = viewMatrix; rtcCameraEye = camera.eye; frameCtx.snapPickOrigin[0] = 0; frameCtx.snapPickOrigin[1] = 0; frameCtx.snapPickOrigin[2] = 0; } gl.uniform3fv(this._uCameraEyeRtc, rtcCameraEye); gl.uniform2fv(this.uVectorA, frameCtx.snapVectorA); gl.uniform2fv(this.uInverseVectorAB, frameCtx.snapInvVectorAB); gl.uniform1i(this._uLayerNumber, frameCtx.snapPickLayerNumber); gl.uniform3fv(this._uCoordinateScaler, coordinateScaler); gl.uniform1i(this._uRenderPass, renderPass); gl.uniform1i(this._uPickInvisible, frameCtx.pickInvisible); let offset = 0; const mat4Size = 4 * 4; this._matricesUniformBlockBufferData.set(rotationMatrix, 0); this._matricesUniformBlockBufferData.set(rtcViewMatrix, offset += mat4Size); this._matricesUniformBlockBufferData.set(camera.projMatrix, offset += mat4Size); this._matricesUniformBlockBufferData.set(state.positionsDecodeMatrix, offset += mat4Size); gl.bindBuffer(gl.UNIFORM_BUFFER, this._matricesUniformBlockBuffer); gl.bufferData(gl.UNIFORM_BUFFER, this._matricesUniformBlockBufferData, gl.DYNAMIC_DRAW); gl.bindBufferBase( gl.UNIFORM_BUFFER, this._matricesUniformBlockBufferBindingPoint, this._matricesUniformBlockBuffer); { const logDepthBufFC = 2.0 / (Math.log(frameCtx.pickZFar + 1.0) / Math.LN2); // TODO: Far from pick project matrix? gl.uniform1f(this._uLogDepthBufFC, logDepthBufFC); } this.setSectionPlanesStateUniforms(batchingLayer); //============================================================= // TODO: Use drawElements count and offset to draw only one entity //============================================================= state.indicesBuf.bind(); gl.drawElements(gl.LINES, state.indicesBuf.numItems, state.indicesBuf.itemType, 0); state.indicesBuf.unbind(); gl.bindVertexArray(null); } _allocate() { super._allocate(); const program = this._program; { this._uLogDepthBufFC = program.getLocation("logDepthBufFC"); } this._uCameraEyeRtc = program.getLocation("uCameraEyeRtc"); this.uVectorA = program.getLocation("snapVectorA"); this.uInverseVectorAB = program.getLocation("snapInvVectorAB"); this._uLayerNumber = program.getLocation("layerNumber"); this._uCoordinateScaler = program.getLocation("coordinateScaler"); } _bindProgram() { this._program.bind(); } _buildVertexShader() { const scene = this._scene; const clipping = scene._sectionPlanesState.getNumAllocatedSectionPlanes() > 0; const src = []; src.push ('#version 300 es'); src.push("// VBO SnapBatchingDepthBufInitRenderer vertex shader"); src.push("#ifdef GL_FRAGMENT_PRECISION_HIGH"); src.push("precision highp float;"); src.push("precision highp int;"); src.push("precision highp usampler2D;"); src.push("precision highp isampler2D;"); src.push("precision highp sampler2D;"); src.push("#else"); src.push("precision mediump float;"); src.push("precision mediump int;"); src.push("precision mediump usampler2D;"); src.push("precision mediump isampler2D;"); src.push("precision mediump sampler2D;"); src.push("#endif"); src.push("uniform int renderPass;"); src.push("in vec4 pickColor;"); src.push("in vec3 position;"); if (scene.entityOffsetsEnabled) { src.push("in vec3 offset;"); } src.push("in float flags;"); src.push("uniform bool pickInvisible;"); this._addMatricesUniformBlockLines(src); src.push("uniform vec3 uCameraEyeRtc;"); src.push("uniform vec2 snapVectorA;"); src.push("uniform vec2 snapInvVectorAB;"); { src.push("uniform float logDepthBufFC;"); src.push("out float vFragDepth;"); src.push("out float isPerspective;"); } src.push("bool isPerspectiveMatrix(mat4 m) {"); src.push(" return (m[2][3] == - 1.0);"); src.push("}"); src.push("vec2 remapClipPos(vec2 clipPos) {"); src.push(" float x = (clipPos.x - snapVectorA.x) * snapInvVectorAB.x;"); src.push(" float y = (clipPos.y - snapVectorA.y) * snapInvVectorAB.y;"); src.push(" return vec2(x, y);"); src.push("}"); src.push("flat out vec4 vPickColor;"); src.push("out vec4 vWorldPosition;"); if (clipping) { src.push("out float vFlags;"); } src.push("out highp vec3 relativeToOriginPosition;"); src.push("void main(void) {"); // pickFlag = NOT_RENDERED | PICK // renderPass = PICK src.push(`int pickFlag = int(flags) >> 12 & 0xF;`); src.push(`if (pickFlag != renderPass) {`); src.push(" gl_Position = vec4(0.0, 0.0, 0.0, 0.0);"); // Cull vertex src.push(" } else {"); src.push(" vec4 worldPosition = worldMatrix * (positionsDecodeMatrix * vec4(position, 1.0)); "); if (scene.entityOffsetsEnabled) { src.push(" worldPosition.xyz = worldPosition.xyz + offset;"); } src.push(" relativeToOriginPosition = worldPosition.xyz;"); src.push(" vec4 viewPosition = viewMatrix * worldPosition; "); src.push(" vWorldPosition = worldPosition;"); if (clipping) { src.push(" vFlags = flags;"); } src.push("vPickColor = pickColor;"); src.push("vec4 clipPos = projMatrix * viewPosition;"); src.push("float tmp = clipPos.w;"); src.push("clipPos.xyzw /= tmp;"); src.push("clipPos.xy = remapClipPos(clipPos.xy);"); src.push("clipPos.xyzw *= tmp;"); { src.push("vFragDepth = 1.0 + clipPos.w;"); src.push("isPerspective = float (isPerspectiveMatrix(projMatrix));"); } src.push("gl_Position = clipPos;"); src.push(" }"); src.push("}"); return src; } _buildFragmentShader() { const scene = this._scene; const sectionPlanesState = scene._sectionPlanesState; const clipping = sectionPlanesState.getNumAllocatedSectionPlanes() > 0; const src = []; src.push ('#version 300 es'); src.push("// VBO SnapBatchingDepthBufInitRenderer fragment shader"); src.push("#ifdef GL_FRAGMENT_PRECISION_HIGH"); src.push("precision highp float;"); src.push("precision highp int;"); src.push("#else"); src.push("precision mediump float;"); src.push("precision mediump int;"); src.push("#endif"); { src.push("in float isPerspective;"); src.push("uniform float logDepthBufFC;"); src.push("in float vFragDepth;"); } src.push("uniform int layerNumber;"); src.push("uniform vec3 coordinateScaler;"); src.push("in vec4 vWorldPosition;"); src.push("flat in vec4 vPickColor;"); if (clipping) { src.push("in float vFlags;"); for (let i = 0; i < sectionPlanesState.getNumAllocatedSectionPlanes(); i++) { src.push("uniform bool sectionPlaneActive" + i + ";"); src.push("uniform vec3 sectionPlanePos" + i + ";"); src.push("uniform vec3 sectionPlaneDir" + i + ";"); } } src.push("in highp vec3 relativeToOriginPosition;"); src.push("layout(location = 0) out highp ivec4 outCoords;"); src.push("layout(location = 1) out highp ivec4 outNormal;"); src.push("layout(location = 2) out lowp uvec4 outPickColor;"); src.push("void main(void) {"); if (clipping) { src.push(" bool clippable = (int(vFlags) >> 16 & 0xF) == 1;"); src.push(" if (clippable) {"); src.push(" float dist = 0.0;"); for (var i = 0; i < sectionPlanesState.getNumAllocatedSectionPlanes(); i++) { src.push(" if (sectionPlaneActive" + i + ") {"); src.push(" dist += clamp(dot(-sectionPlaneDir" + i + ".xyz, vWorldPosition.xyz - sectionPlanePos" + i + ".xyz), 0.0, 1000.0);"); src.push(" }"); } src.push(" if (dist > 0.0) { discard; }"); src.push(" }"); } { src.push(" float dx = dFdx(vFragDepth);"); src.push(" float dy = dFdy(vFragDepth);"); src.push(" float diff = sqrt(dx*dx+dy*dy);"); src.push(" gl_FragDepth = isPerspective == 0.0 ? gl_FragCoord.z : log2( vFragDepth + diff ) * logDepthBufFC * 0.5;"); } src.push("outCoords = ivec4(relativeToOriginPosition.xyz*coordinateScaler.xyz, -layerNumber);"); src.push("vec3 xTangent = dFdx( vWorldPosition.xyz );"); src.push("vec3 yTangent = dFdy( vWorldPosition.xyz );"); src.push("vec3 worldNormal = normalize( cross( xTangent, yTangent ) );"); src.push(`outNormal = ivec4(worldNormal * float(${math.MAX_INT}), 1.0);`); src.push("outPickColor = uvec4(vPickColor);"); src.push("}"); return src; } webglContextRestored() { this._program = null; } destroy() { if (this._program) { this._program.destroy(); } this._program = null; } } const tempVec3a$x = math.vec3(); const tempVec3b$t = math.vec3(); const tempVec3c$o = math.vec3(); const tempVec3d$7 = math.vec3(); const tempMat4a$l = math.mat4(); /** * @private */ class VBOBatchingLinesSnapRenderer extends VBORenderer{ _getHash() { return this._scene._sectionPlanesState.getHash() + (this._scene.pointsMaterial.hash); } drawLayer(frameCtx, batchingLayer, renderPass) { if (!this._program) { this._allocate(); if (this.errors) { return; } } if (frameCtx.lastProgramId !== this._program.id) { frameCtx.lastProgramId = this._program.id; this._bindProgram(); } const model = batchingLayer.model; const scene = model.scene; const camera = scene.camera; const gl = scene.canvas.gl; const state = batchingLayer._state; const origin = batchingLayer._state.origin; const {position, rotationMatrix} = model; const aabb = batchingLayer.aabb; // Per-layer AABB for best RTC accuracy const viewMatrix = frameCtx.pickViewMatrix || camera.viewMatrix; if (this._vaoCache.has(batchingLayer)) { gl.bindVertexArray(this._vaoCache.get(batchingLayer)); } else { this._vaoCache.set(batchingLayer, this._makeVAO(state)); } const coordinateScaler = tempVec3a$x; coordinateScaler[0] = math.safeInv(aabb[3] - aabb[0]) * math.MAX_INT; coordinateScaler[1] = math.safeInv(aabb[4] - aabb[1]) * math.MAX_INT; coordinateScaler[2] = math.safeInv(aabb[5] - aabb[2]) * math.MAX_INT; frameCtx.snapPickCoordinateScale[0] = math.safeInv(coordinateScaler[0]); frameCtx.snapPickCoordinateScale[1] = math.safeInv(coordinateScaler[1]); frameCtx.snapPickCoordinateScale[2] = math.safeInv(coordinateScaler[2]); let rtcViewMatrix; let rtcCameraEye; if (origin || position[0] !== 0 || position[1] !== 0 || position[2] !== 0) { const rtcOrigin = tempVec3b$t; if (origin) { const rotatedOrigin = tempVec3c$o; math.transformPoint3(rotationMatrix, origin, rotatedOrigin); rtcOrigin[0] = rotatedOrigin[0]; rtcOrigin[1] = rotatedOrigin[1]; rtcOrigin[2] = rotatedOrigin[2]; } else { rtcOrigin[0] = 0; rtcOrigin[1] = 0; rtcOrigin[2] = 0; } rtcOrigin[0] += position[0]; rtcOrigin[1] += position[1]; rtcOrigin[2] += position[2]; rtcViewMatrix = createRTCViewMat(viewMatrix, rtcOrigin, tempMat4a$l); rtcCameraEye = tempVec3d$7; rtcCameraEye[0] = camera.eye[0] - rtcOrigin[0]; rtcCameraEye[1] = camera.eye[1] - rtcOrigin[1]; rtcCameraEye[2] = camera.eye[2] - rtcOrigin[2]; frameCtx.snapPickOrigin[0] = rtcOrigin[0]; frameCtx.snapPickOrigin[1] = rtcOrigin[1]; frameCtx.snapPickOrigin[2] = rtcOrigin[2]; } else { rtcViewMatrix = viewMatrix; rtcCameraEye = camera.eye; frameCtx.snapPickOrigin[0] = 0; frameCtx.snapPickOrigin[1] = 0; frameCtx.snapPickOrigin[2] = 0; } gl.uniform3fv(this._uCameraEyeRtc, rtcCameraEye); gl.uniform2fv(this.uVectorA, frameCtx.snapVectorA); gl.uniform2fv(this.uInverseVectorAB, frameCtx.snapInvVectorAB); gl.uniform1i(this._uLayerNumber, frameCtx.snapPickLayerNumber); gl.uniform3fv(this._uCoordinateScaler, coordinateScaler); gl.uniform1i(this._uRenderPass, renderPass); gl.uniform1i(this._uPickInvisible, frameCtx.pickInvisible); let offset = 0; const mat4Size = 4 * 4; this._matricesUniformBlockBufferData.set(rotationMatrix, 0); this._matricesUniformBlockBufferData.set(rtcViewMatrix, offset += mat4Size); this._matricesUniformBlockBufferData.set(camera.projMatrix, offset += mat4Size); this._matricesUniformBlockBufferData.set(state.positionsDecodeMatrix, offset += mat4Size); gl.bindBuffer(gl.UNIFORM_BUFFER, this._matricesUniformBlockBuffer); gl.bufferData(gl.UNIFORM_BUFFER, this._matricesUniformBlockBufferData, gl.DYNAMIC_DRAW); gl.bindBufferBase( gl.UNIFORM_BUFFER, this._matricesUniformBlockBufferBindingPoint, this._matricesUniformBlockBuffer); { const logDepthBufFC = 2.0 / (Math.log(frameCtx.pickZFar + 1.0) / Math.LN2); // TODO: Far from pick project matrix? gl.uniform1f(this._uLogDepthBufFC, logDepthBufFC); } this.setSectionPlanesStateUniforms(batchingLayer); //============================================================= // TODO: Use drawElements count and offset to draw only one entity //============================================================= if (frameCtx.snapMode === "edge") { state.indicesBuf.bind(); gl.drawElements(gl.LINES, state.indicesBuf.numItems, state.indicesBuf.itemType, 0); state.indicesBuf.unbind(); // needed? } else { gl.drawArrays(gl.POINTS, 0, state.positionsBuf.numItems); } gl.bindVertexArray(null); } _allocate() { super._allocate(); const program = this._program; { this._uLogDepthBufFC = program.getLocation("logDepthBufFC"); } this._uCameraEyeRtc = program.getLocation("uCameraEyeRtc"); this.uVectorA = program.getLocation("snapVectorA"); this.uInverseVectorAB = program.getLocation("snapInvVectorAB"); this._uLayerNumber = program.getLocation("layerNumber"); this._uCoordinateScaler = program.getLocation("coordinateScaler"); } _bindProgram() { this._program.bind(); } _buildVertexShader() { const scene = this._scene; const clipping = scene._sectionPlanesState.getNumAllocatedSectionPlanes() > 0; scene.pointsMaterial._state; const src = []; src.push ('#version 300 es'); src.push("// SnapBatchingDepthRenderer vertex shader"); src.push("#ifdef GL_FRAGMENT_PRECISION_HIGH"); src.push("precision highp float;"); src.push("precision highp int;"); src.push("precision highp usampler2D;"); src.push("precision highp isampler2D;"); src.push("precision highp sampler2D;"); src.push("#else"); src.push("precision mediump float;"); src.push("precision mediump int;"); src.push("precision mediump usampler2D;"); src.push("precision mediump isampler2D;"); src.push("precision mediump sampler2D;"); src.push("#endif"); src.push("uniform int renderPass;"); src.push("in vec3 position;"); if (scene.entityOffsetsEnabled) { src.push("in vec3 offset;"); } src.push("in float flags;"); src.push("uniform bool pickInvisible;"); this._addMatricesUniformBlockLines(src); src.push("uniform vec3 uCameraEyeRtc;"); src.push("uniform vec2 snapVectorA;"); src.push("uniform vec2 snapInvVectorAB;"); { src.push("uniform float logDepthBufFC;"); src.push("out float vFragDepth;"); src.push("bool isPerspectiveMatrix(mat4 m) {"); src.push(" return (m[2][3] == - 1.0);"); src.push("}"); src.push("out float isPerspective;"); } src.push("vec2 remapClipPos(vec2 clipPos) {"); src.push(" float x = (clipPos.x - snapVectorA.x) * snapInvVectorAB.x;"); src.push(" float y = (clipPos.y - snapVectorA.y) * snapInvVectorAB.y;"); src.push(" return vec2(x, y);"); src.push("}"); if (clipping) { src.push("out vec4 vWorldPosition;"); src.push("out float vFlags;"); } src.push("out highp vec3 relativeToOriginPosition;"); src.push("void main(void) {"); // pickFlag = NOT_RENDERED | PICK // renderPass = PICK src.push(`int pickFlag = int(flags) >> 12 & 0xF;`); src.push(`if (pickFlag != renderPass) {`); src.push(" gl_Position = vec4(2.0, 0.0, 0.0, 0.0);"); // Cull vertex src.push(" } else {"); src.push(" vec4 worldPosition = worldMatrix * (positionsDecodeMatrix * vec4(position, 1.0)); "); if (scene.entityOffsetsEnabled) { src.push(" worldPosition.xyz = worldPosition.xyz + offset;"); } src.push("relativeToOriginPosition = worldPosition.xyz;"); src.push(" vec4 viewPosition = viewMatrix * worldPosition; "); if (clipping) { src.push(" vWorldPosition = worldPosition;"); src.push(" vFlags = flags;"); } src.push("vec4 clipPos = projMatrix * viewPosition;"); src.push("float tmp = clipPos.w;"); src.push("clipPos.xyzw /= tmp;"); src.push("clipPos.xy = remapClipPos(clipPos.xy);"); src.push("clipPos.xyzw *= tmp;"); { src.push("vFragDepth = 1.0 + clipPos.w;"); src.push("isPerspective = float (isPerspectiveMatrix(projMatrix));"); } src.push("gl_Position = clipPos;"); src.push("gl_PointSize = 1.0;"); // Windows needs this? src.push(" }"); src.push("}"); return src; } _buildFragmentShader() { const scene = this._scene; const sectionPlanesState = scene._sectionPlanesState; const clipping = sectionPlanesState.getNumAllocatedSectionPlanes() > 0; const src = []; src.push ('#version 300 es'); src.push("// SnapBatchingDepthRenderer fragment shader"); src.push("#ifdef GL_FRAGMENT_PRECISION_HIGH"); src.push("precision highp float;"); src.push("precision highp int;"); src.push("#else"); src.push("precision mediump float;"); src.push("precision mediump int;"); src.push("#endif"); { src.push("in float isPerspective;"); src.push("uniform float logDepthBufFC;"); src.push("in float vFragDepth;"); } src.push("uniform int layerNumber;"); src.push("uniform vec3 coordinateScaler;"); if (clipping) { src.push("in vec4 vWorldPosition;"); src.push("in float vFlags;"); for (let i = 0; i < sectionPlanesState.getNumAllocatedSectionPlanes(); i++) { src.push("uniform bool sectionPlaneActive" + i + ";"); src.push("uniform vec3 sectionPlanePos" + i + ";"); src.push("uniform vec3 sectionPlaneDir" + i + ";"); } } src.push("in highp vec3 relativeToOriginPosition;"); src.push("out highp ivec4 outCoords;"); src.push("void main(void) {"); if (clipping) { src.push(" bool clippable = (int(vFlags) >> 16 & 0xF) == 1;"); src.push(" if (clippable) {"); src.push(" float dist = 0.0;"); for (var i = 0; i < sectionPlanesState.getNumAllocatedSectionPlanes(); i++) { src.push(" if (sectionPlaneActive" + i + ") {"); src.push(" dist += clamp(dot(-sectionPlaneDir" + i + ".xyz, vWorldPosition.xyz - sectionPlanePos" + i + ".xyz), 0.0, 1000.0);"); src.push(" }"); } src.push(" if (dist > 0.0) { discard; }"); src.push(" }"); } { src.push(" gl_FragDepth = isPerspective == 0.0 ? gl_FragCoord.z : log2( vFragDepth ) * logDepthBufFC * 0.5;"); } src.push("outCoords = ivec4(relativeToOriginPosition.xyz*coordinateScaler.xyz, layerNumber);"); src.push("}"); return src; } webglContextRestored() { this._program = null; } destroy() { if (this._program) { this._program.destroy(); } this._program = null; } } /** * @private */ class VBOBatchingLinesRenderers { constructor(scene) { this._scene = scene; } _compile() { if (this._colorRenderer && (!this._colorRenderer.getValid())) { this._colorRenderer.destroy(); this._colorRenderer = null; } if (this._silhouetteRenderer && (!this._silhouetteRenderer.getValid())) { this._silhouetteRenderer.destroy(); this._silhouetteRenderer = null; } if (this._snapInitRenderer && (!this._snapInitRenderer.getValid())) { this._snapInitRenderer.destroy(); this._snapInitRenderer = null; } if (this._snapRenderer && (!this._snapRenderer.getValid())) { this._snapRenderer.destroy(); this._snapRenderer = null; } } get colorRenderer() { if (!this._colorRenderer) { this._colorRenderer = new VBOBatchingLinesColorRenderer(this._scene, false); } return this._colorRenderer; } get silhouetteRenderer() { if (!this._silhouetteRenderer) { this._silhouetteRenderer = new VBOBatchingLinesSilhouetteRenderer(this._scene); } return this._silhouetteRenderer; } get snapInitRenderer() { if (!this._snapInitRenderer) { this._snapInitRenderer = new VBOBatchingLinesSnapInitRenderer(this._scene, false); } return this._snapInitRenderer; } get snapRenderer() { if (!this._snapRenderer) { this._snapRenderer = new VBOBatchingLinesSnapRenderer(this._scene); } return this._snapRenderer; } _destroy() { if (this._colorRenderer) { this._colorRenderer.destroy(); } if (this._silhouetteRenderer) { this._silhouetteRenderer.destroy(); } if (this._snapInitRenderer) { this._snapInitRenderer.destroy(); } if (this._snapRenderer) { this._snapRenderer.destroy(); } } } const cachedRenderers$4 = {}; /** * @private */ function getRenderers$5(scene) { const sceneId = scene.id; let batchingRenderers = cachedRenderers$4[sceneId]; if (!batchingRenderers) { batchingRenderers = new VBOBatchingLinesRenderers(scene); cachedRenderers$4[sceneId] = batchingRenderers; batchingRenderers._compile(); scene.on("compile", () => { batchingRenderers._compile(); }); scene.on("destroyed", () => { delete cachedRenderers$4[sceneId]; batchingRenderers._destroy(); }); } return batchingRenderers; } /** * @private */ class VBOBatchingLinesBuffer { constructor(maxGeometryBatchSize = 5000000) { if (maxGeometryBatchSize > 5000000) { maxGeometryBatchSize = 5000000; } this.maxVerts = maxGeometryBatchSize; this.maxIndices = maxGeometryBatchSize * 3; // Rough rule-of-thumb this.positions = []; this.colors = []; this.offsets = []; this.indices = []; } } /** * @private */ class VBOBatchingLinesLayer { /** * @param model * @param cfg * @param cfg.layerIndex * @param cfg.positionsDecodeMatrix * @param cfg.maxGeometryBatchSize * @param cfg.origin * @param cfg.scratchMemory */ constructor(cfg) { // console.info("Creating VBOBatchingLinesLayer"); /** * Index of this LinesBatchingLayer in {@link VBOSceneModel#_layerList}. * @type {Number} */ this.layerIndex = cfg.layerIndex; this._renderers = getRenderers$5(cfg.model.scene); this.model = cfg.model; this._buffer = new VBOBatchingLinesBuffer(cfg.maxGeometryBatchSize); this._scratchMemory = cfg.scratchMemory; this._state = new RenderState({ positionsBuf: null, offsetsBuf: null, colorsBuf: null, flagsBuf: null, indicesBuf: null, positionsDecodeMatrix: math.mat4(), origin: null }); // These counts are used to avoid unnecessary render passes this._numPortions = 0; this._numVisibleLayerPortions = 0; this._numTransparentLayerPortions = 0; this._numXRayedLayerPortions = 0; this._numSelectedLayerPortions = 0; this._numHighlightedLayerPortions = 0; this._numClippableLayerPortions = 0; this._numEdgesLayerPortions = 0; this._numPickableLayerPortions = 0; this._numCulledLayerPortions = 0; this._modelAABB = math.collapseAABB3(); // Model-space AABB this._portions = []; this._meshes = []; this._numVerts = 0; this._aabb = math.collapseAABB3(); this.aabbDirty = true; this._finalized = false; if (cfg.positionsDecodeMatrix) { this._state.positionsDecodeMatrix.set(cfg.positionsDecodeMatrix); this._preCompressedPositionsExpected = true; } else { this._preCompressedPositionsExpected = false; } if (cfg.origin) { this._state.origin = math.vec3(cfg.origin); } /** * The type of primitives in this layer. */ this.primitive = cfg.primitive; } get aabb() { if (this.aabbDirty) { math.collapseAABB3(this._aabb); for (let i = 0, len = this._meshes.length; i < len; i++) { math.expandAABB3(this._aabb, this._meshes[i].aabb); } this.aabbDirty = false; } return this._aabb; } /** * Tests if there is room for another portion in this LinesBatchingLayer. * * @param lenPositions Number of positions we'd like to create in the portion. * @param lenIndices Number of indices we'd like to create in this portion. * @returns {Boolean} True if OK to create another portion. */ canCreatePortion(lenPositions, lenIndices) { if (this._finalized) { throw "Already finalized"; } return ((this._buffer.positions.length + lenPositions) < (this._buffer.maxVerts * 3) && (this._buffer.indices.length + lenIndices) < (this._buffer.maxIndices)); } /** * Creates a new portion within this LinesBatchingLayer, returns the new portion ID. * * Gives the portion the specified geometry, color and matrix. * * @param mesh The SceneModelMesh that owns the portion * @param cfg.positions Flat float Local-space positions array. * @param cfg.positionsCompressed Flat quantized positions array - decompressed with TrianglesBatchingLayer positionsDecodeMatrix * @param cfg.indices Flat int indices array. * @param cfg.color Quantized RGB color [0..255,0..255,0..255,0..255] * @param cfg.opacity Opacity [0..255] * @param [cfg.meshMatrix] Flat float 4x4 matrix * @param cfg.aabb Flat float AABB World-space AABB * @param cfg.pickColor Quantized pick color * @returns {number} Portion ID */ createPortion(mesh, cfg) { if (this._finalized) { throw "Already finalized"; } const positions = cfg.positions; const positionsCompressed = cfg.positionsCompressed; const indices = cfg.indices; const color = cfg.color; const opacity = cfg.opacity; const buffer = this._buffer; const positionsIndex = buffer.positions.length; const vertsIndex = positionsIndex / 3; let numVerts; math.expandAABB3(this._modelAABB, cfg.aabb); if (this._preCompressedPositionsExpected) { if (!positionsCompressed) { throw "positionsCompressed expected"; } numVerts = positionsCompressed.length / 3; for (let i = 0, len = positionsCompressed.length; i < len; i++) { buffer.positions.push(positionsCompressed[i]); } } else { if (!positions) { throw "positions expected"; } numVerts = positions.length / 3; for (let i = 0, len = positions.length; i < len; i++) { buffer.positions.push(positions[i]); } } if (color) { const r = color[0]; // Color is pre-quantized by VBOSceneModel const g = color[1]; const b = color[2]; const a = opacity; for (let i = 0; i < numVerts; i++) { buffer.colors.push(r); buffer.colors.push(g); buffer.colors.push(b); buffer.colors.push(a); } } if (indices) { for (let i = 0, len = indices.length; i < len; i++) { buffer.indices.push(indices[i] + vertsIndex); } } if (this.model.scene.entityOffsetsEnabled) { for (let i = 0; i < numVerts; i++) { buffer.offsets.push(0); buffer.offsets.push(0); buffer.offsets.push(0); } } const portionId = this._portions.length / 2; this._portions.push(vertsIndex); this._portions.push(numVerts); this._numPortions++; this.model.numPortions++; this._numVerts += numVerts; this._meshes.push(mesh); return portionId; } /** * Builds batch VBOs from appended geometries. * No more portions can then be created. */ finalize() { if (this._finalized) { return; } const state = this._state; const gl = this.model.scene.canvas.gl; const buffer = this._buffer; if (buffer.positions.length > 0) { if (this._preCompressedPositionsExpected) { const positions = new Uint16Array(buffer.positions); state.positionsBuf = new ArrayBuf(gl, gl.ARRAY_BUFFER, positions, buffer.positions.length, 3, gl.STATIC_DRAW); } else { const positions = new Float32Array(buffer.positions); const quantizedPositions = quantizePositions(positions, this._modelAABB, state.positionsDecodeMatrix); state.positionsBuf = new ArrayBuf(gl, gl.ARRAY_BUFFER, quantizedPositions, buffer.positions.length, 3, gl.STATIC_DRAW); } } if (buffer.colors.length > 0) { const colors = new Uint8Array(buffer.colors); let normalized = false; state.colorsBuf = new ArrayBuf(gl, gl.ARRAY_BUFFER, colors, buffer.colors.length, 4, gl.DYNAMIC_DRAW, normalized); } if (buffer.colors.length > 0) { // Because we build flags arrays here, get their length from the colors array const flagsLength = buffer.colors.length / 4; const flags = new Float32Array(flagsLength); let notNormalized = false; state.flagsBuf = new ArrayBuf(gl, gl.ARRAY_BUFFER, flags, flags.length, 1, gl.DYNAMIC_DRAW, notNormalized); } if (this.model.scene.entityOffsetsEnabled) { if (buffer.offsets.length > 0) { const offsets = new Float32Array(buffer.offsets); state.offsetsBuf = new ArrayBuf(gl, gl.ARRAY_BUFFER, offsets, buffer.offsets.length, 3, gl.DYNAMIC_DRAW); } } if (buffer.indices.length > 0) { const indices = new Uint32Array(buffer.indices); state.indicesBuf = new ArrayBuf(gl, gl.ELEMENT_ARRAY_BUFFER, indices, buffer.indices.length, 1, gl.STATIC_DRAW); } this._buffer = null; this._finalized = true; } initFlags(portionId, flags, meshTransparent) { if (flags & ENTITY_FLAGS.VISIBLE) { this._numVisibleLayerPortions++; this.model.numVisibleLayerPortions++; } if (flags & ENTITY_FLAGS.HIGHLIGHTED) { this._numHighlightedLayerPortions++; this.model.numHighlightedLayerPortions++; } if (flags & ENTITY_FLAGS.XRAYED) { this._numXRayedLayerPortions++; this.model.numXRayedLayerPortions++; } if (flags & ENTITY_FLAGS.SELECTED) { this._numSelectedLayerPortions++; this.model.numSelectedLayerPortions++; } if (flags & ENTITY_FLAGS.CLIPPABLE) { this._numClippableLayerPortions++; this.model.numClippableLayerPortions++; } if (flags & ENTITY_FLAGS.EDGES) { this._numEdgesLayerPortions++; this.model.numEdgesLayerPortions++; } if (flags & ENTITY_FLAGS.PICKABLE) { this._numPickableLayerPortions++; this.model.numPickableLayerPortions++; } if (flags & ENTITY_FLAGS.CULLED) { this._numCulledLayerPortions++; this.model.numCulledLayerPortions++; } if (meshTransparent) { this._numTransparentLayerPortions++; this.model.numTransparentLayerPortions++; } const deferred = true; this._setFlags(portionId, flags, meshTransparent, deferred); } flushInitFlags() { this._setDeferredFlags(); } setVisible(portionId, flags, transparent) { if (!this._finalized) { throw "Not finalized"; } if (flags & ENTITY_FLAGS.VISIBLE) { this._numVisibleLayerPortions++; this.model.numVisibleLayerPortions++; } else { this._numVisibleLayerPortions--; this.model.numVisibleLayerPortions--; } this._setFlags(portionId, flags, transparent); } setHighlighted(portionId, flags, transparent) { if (!this._finalized) { throw "Not finalized"; } if (flags & ENTITY_FLAGS.HIGHLIGHTED) { this._numHighlightedLayerPortions++; this.model.numHighlightedLayerPortions++; } else { this._numHighlightedLayerPortions--; this.model.numHighlightedLayerPortions--; } this._setFlags(portionId, flags, transparent); } setXRayed(portionId, flags, transparent) { if (!this._finalized) { throw "Not finalized"; } if (flags & ENTITY_FLAGS.XRAYED) { this._numXRayedLayerPortions++; this.model.numXRayedLayerPortions++; } else { this._numXRayedLayerPortions--; this.model.numXRayedLayerPortions--; } this._setFlags(portionId, flags, transparent); } setSelected(portionId, flags, transparent) { if (!this._finalized) { throw "Not finalized"; } if (flags & ENTITY_FLAGS.SELECTED) { this._numSelectedLayerPortions++; this.model.numSelectedLayerPortions++; } else { this._numSelectedLayerPortions--; this.model.numSelectedLayerPortions--; } this._setFlags(portionId, flags, transparent); } setEdges(portionId, flags, transparent) { if (!this._finalized) { throw "Not finalized"; } if (flags & ENTITY_FLAGS.EDGES) { this._numEdgesLayerPortions++; this.model.numEdgesLayerPortions++; } else { this._numEdgesLayerPortions--; this.model.numEdgesLayerPortions--; } this._setFlags(portionId, flags, transparent); } setClippable(portionId, flags) { if (!this._finalized) { throw "Not finalized"; } if (flags & ENTITY_FLAGS.CLIPPABLE) { this._numClippableLayerPortions++; this.model.numClippableLayerPortions++; } else { this._numClippableLayerPortions--; this.model.numClippableLayerPortions--; } this._setFlags(portionId, flags); } setCulled(portionId, flags, transparent) { if (!this._finalized) { throw "Not finalized"; } if (flags & ENTITY_FLAGS.CULLED) { this._numCulledLayerPortions++; this.model.numCulledLayerPortions++; } else { this._numCulledLayerPortions--; this.model.numCulledLayerPortions--; } this._setFlags(portionId, flags, transparent); } setCollidable(portionId, flags) { if (!this._finalized) { throw "Not finalized"; } } setPickable(portionId, flags, transparent) { if (!this._finalized) { throw "Not finalized"; } if (flags & ENTITY_FLAGS.PICKABLE) { this._numPickableLayerPortions++; this.model.numPickableLayerPortions++; } else { this._numPickableLayerPortions--; this.model.numPickableLayerPortions--; } this._setFlags(portionId, flags, transparent); } setColor(portionId, color) { if (!this._finalized) { throw "Not finalized"; } const portionsIdx = portionId * 2; const vertexBase = this._portions[portionsIdx]; const numVerts = this._portions[portionsIdx + 1]; const firstColor = vertexBase * 4; const lenColor = numVerts * 4; const tempArray = this._scratchMemory.getUInt8Array(lenColor); const r = color[0]; const g = color[1]; const b = color[2]; const a = color[3]; for (let i = 0; i < lenColor; i += 4) { tempArray[i + 0] = r; tempArray[i + 1] = g; tempArray[i + 2] = b; tempArray[i + 3] = a; } this._state.colorsBuf.setData(tempArray, firstColor, lenColor); } setTransparent(portionId, flags, transparent) { if (transparent) { this._numTransparentLayerPortions++; this.model.numTransparentLayerPortions++; } else { this._numTransparentLayerPortions--; this.model.numTransparentLayerPortions--; } this._setFlags(portionId, flags, transparent); } /** * flags are 4bits values encoded on a 32bit base. color flag on the first 4 bits, silhouette flag on the next 4 bits and so on for edge, pick and clippable. */ _setFlags(portionId, flags, transparent, deferred = false) { if (!this._finalized) { throw "Not finalized"; } const portionsIdx = portionId * 2; const vertexBase = this._portions[portionsIdx]; const numVerts = this._portions[portionsIdx + 1]; const firstFlag = vertexBase; const lenFlags = numVerts; const visible = !!(flags & ENTITY_FLAGS.VISIBLE); const xrayed = !!(flags & ENTITY_FLAGS.XRAYED); const highlighted = !!(flags & ENTITY_FLAGS.HIGHLIGHTED); const selected = !!(flags & ENTITY_FLAGS.SELECTED); // no edges const pickable = !!(flags & ENTITY_FLAGS.PICKABLE); const culled = !!(flags & ENTITY_FLAGS.CULLED); let colorFlag; if (!visible || culled || xrayed || (highlighted && !this.model.scene.highlightMaterial.glowThrough) || (selected && !this.model.scene.selectedMaterial.glowThrough)) { colorFlag = RENDER_PASSES.NOT_RENDERED; } else { if (transparent) { colorFlag = RENDER_PASSES.COLOR_TRANSPARENT; } else { colorFlag = RENDER_PASSES.COLOR_OPAQUE; } } let silhouetteFlag; if (!visible || culled) { silhouetteFlag = RENDER_PASSES.NOT_RENDERED; } else if (selected) { silhouetteFlag = RENDER_PASSES.SILHOUETTE_SELECTED; } else if (highlighted) { silhouetteFlag = RENDER_PASSES.SILHOUETTE_HIGHLIGHTED; } else if (xrayed) { silhouetteFlag = RENDER_PASSES.SILHOUETTE_XRAYED; } else { silhouetteFlag = RENDER_PASSES.NOT_RENDERED; } let pickFlag = (visible && !culled && pickable) ? RENDER_PASSES.PICK : RENDER_PASSES.NOT_RENDERED; const clippableFlag = !!(flags & ENTITY_FLAGS.CLIPPABLE) ? 1 : 0; if (deferred) { // Avoid zillions of individual WebGL bufferSubData calls - buffer them to apply in one shot if (!this._deferredFlagValues) { this._deferredFlagValues = new Float32Array(this._numVerts); } for (let i = firstFlag, len = (firstFlag + lenFlags); i < len; i++) { let vertFlag = 0; vertFlag |= colorFlag; vertFlag |= silhouetteFlag << 4; // no edges vertFlag |= pickFlag << 12; vertFlag |= clippableFlag << 16; this._deferredFlagValues[i] = vertFlag; } } else if (this._state.flagsBuf) { const tempArray = this._scratchMemory.getFloat32Array(lenFlags); for (let i = 0; i < lenFlags; i++) { let vertFlag = 0; vertFlag |= colorFlag; vertFlag |= silhouetteFlag << 4; // no edges vertFlag |= pickFlag << 12; vertFlag |= clippableFlag << 16; tempArray[i] = vertFlag; } this._state.flagsBuf.setData(tempArray, firstFlag, lenFlags); } } _setDeferredFlags() { if (this._deferredFlagValues) { this._state.flagsBuf.setData(this._deferredFlagValues); this._deferredFlagValues = null; } } setOffset(portionId, offset) { if (!this._finalized) { throw "Not finalized"; } if (!this.model.scene.entityOffsetsEnabled) { this.model.error("Entity#offset not enabled for this Viewer"); // See Viewer entityOffsetsEnabled return; } const portionsIdx = portionId * 2; const vertexBase = this._portions[portionsIdx]; const numVerts = this._portions[portionsIdx + 1]; const firstOffset = vertexBase * 3; const lenOffsets = numVerts * 3; const tempArray = this._scratchMemory.getFloat32Array(lenOffsets); const x = offset[0]; const y = offset[1]; const z = offset[2]; for (let i = 0; i < lenOffsets; i += 3) { tempArray[i + 0] = x; tempArray[i + 1] = y; tempArray[i + 2] = z; } this._state.offsetsBuf.setData(tempArray, firstOffset, lenOffsets); } //-- RENDERING ---------------------------------------------------------------------------------------------- drawColorOpaque(renderFlags, frameCtx) { if (this._numCulledLayerPortions === this._numPortions || this._numVisibleLayerPortions === 0 || this._numTransparentLayerPortions === this._numPortions || this._numXRayedLayerPortions === this._numPortions) { return; } if (this._renderers.colorRenderer) { this._renderers.colorRenderer.drawLayer(frameCtx, this, RENDER_PASSES.COLOR_OPAQUE); } } drawColorTransparent(renderFlags, frameCtx) { if (this._numCulledLayerPortions === this._numPortions || this._numVisibleLayerPortions === 0 || this._numTransparentLayerPortions === 0 || this._numXRayedLayerPortions === this._numPortions) { return; } if (this._renderers.colorRenderer) { this._renderers.colorRenderer.drawLayer(frameCtx, this, RENDER_PASSES.COLOR_TRANSPARENT); } } drawDepth(renderFlags, frameCtx) { } drawNormals(renderFlags, frameCtx) { } drawSilhouetteXRayed(renderFlags, frameCtx) { if (this._numCulledLayerPortions === this._numPortions || this._numVisibleLayerPortions === 0 || this._numXRayedLayerPortions === 0) { return; } if (this._renderers.silhouetteRenderer) { this._renderers.silhouetteRenderer.drawLayer(frameCtx, this, RENDER_PASSES.SILHOUETTE_XRAYED); } } drawSilhouetteHighlighted(renderFlags, frameCtx) { if (this._numCulledLayerPortions === this._numPortions || this._numVisibleLayerPortions === 0 || this._numHighlightedLayerPortions === 0) { return; } if (this._renderers.silhouetteRenderer) { this._renderers.silhouetteRenderer.drawLayer(frameCtx, this, RENDER_PASSES.SILHOUETTE_HIGHLIGHTED); } } drawSilhouetteSelected(renderFlags, frameCtx) { if (this._numCulledLayerPortions === this._numPortions || this._numVisibleLayerPortions === 0 || this._numSelectedLayerPortions === 0) { return; } if (this._renderers.silhouetteRenderer) { this._renderers.silhouetteRenderer.drawLayer(frameCtx, this, RENDER_PASSES.SILHOUETTE_SELECTED); } } drawEdgesColorOpaque(renderFlags, frameCtx) { } drawEdgesColorTransparent(renderFlags, frameCtx) { } drawEdgesHighlighted(renderFlags, frameCtx) { } drawEdgesSelected(renderFlags, frameCtx) { } drawEdgesXRayed(renderFlags, frameCtx) { } drawPickMesh(frameCtx) { } drawPickDepths(frameCtx) { } drawPickNormals(frameCtx) { } drawSnapInit(renderFlags, frameCtx) { if (this._numCulledLayerPortions === this._numPortions || this._numVisibleLayerPortions === 0) { return; } if (this._renderers.snapInitRenderer) { this._renderers.snapInitRenderer.drawLayer(frameCtx, this, RENDER_PASSES.PICK); } } drawSnap(renderFlags, frameCtx) { if (this._numCulledLayerPortions === this._numPortions || this._numVisibleLayerPortions === 0) { return; } if (this._renderers.snapRenderer) { this._renderers.snapRenderer.drawLayer(frameCtx, this, RENDER_PASSES.PICK); } } drawOcclusion(frameCtx) { } drawShadow(frameCtx) { } destroy() { const state = this._state; if (state.positionsBuf) { state.positionsBuf.destroy(); state.positionsBuf = null; } if (state.offsetsBuf) { state.offsetsBuf.destroy(); state.offsetsBuf = null; } if (state.colorsBuf) { state.colorsBuf.destroy(); state.colorsBuf = null; } if (state.flagsBuf) { state.flagsBuf.destroy(); state.flagsBuf = null; } if (state.indicesBuf) { state.indicesBuf.destroy(); state.indicesBuf = null; } state.destroy(); } } /** * @private */ class VBOInstancingLinesRenderer extends VBORenderer { constructor(scene, withSAO) { super(scene, withSAO, {instancing: true}); } _draw(drawCfg) { const {gl} = this._scene.canvas; const { state, frameCtx, incrementDrawState, } = drawCfg; gl.drawElementsInstanced(gl.LINES, state.indicesBuf.numItems, state.indicesBuf.itemType, 0, state.numInstances); if (incrementDrawState) { frameCtx.drawElements++; } } } /** * @private */ class VBOInstancingLinesColorRenderer extends VBOInstancingLinesRenderer { drawLayer(frameCtx, layer, renderPass) { super.drawLayer(frameCtx, layer, renderPass, {incrementDrawState: true}); } _buildVertexShader() { const scene = this._scene; const sectionPlanesState = scene._sectionPlanesState; const clipping = sectionPlanesState.getNumAllocatedSectionPlanes() > 0; const src = []; src.push('#version 300 es'); src.push("// Lines instancing color vertex shader"); src.push("uniform int renderPass;"); src.push("in vec3 position;"); src.push("in vec4 color;"); src.push("in float flags;"); if (scene.entityOffsetsEnabled) { src.push("in vec3 offset;"); } src.push("in vec4 modelMatrixCol0;"); // Modeling matrix src.push("in vec4 modelMatrixCol1;"); src.push("in vec4 modelMatrixCol2;"); this._addMatricesUniformBlockLines(src); if (scene.logarithmicDepthBufferEnabled) { src.push("uniform float logDepthBufFC;"); src.push("out float vFragDepth;"); } src.push("uniform vec4 lightAmbient;"); if (clipping) { src.push("out vec4 vWorldPosition;"); src.push("out float vFlags;"); } src.push("out vec4 vColor;"); src.push("void main(void) {"); // colorFlag = NOT_RENDERED | COLOR_OPAQUE | COLOR_TRANSPARENT // renderPass = COLOR_OPAQUE | COLOR_TRANSPARENT src.push(`int colorFlag = int(flags) & 0xF;`); src.push(`if (colorFlag != renderPass) {`); src.push(" gl_Position = vec4(0.0, 0.0, 0.0, 0.0);"); // Cull vertex src.push("} else {"); src.push("vec4 worldPosition = positionsDecodeMatrix * vec4(position, 1.0); "); src.push("worldPosition = worldMatrix * vec4(dot(worldPosition, modelMatrixCol0), dot(worldPosition, modelMatrixCol1), dot(worldPosition, modelMatrixCol2), 1.0);"); if (scene.entityOffsetsEnabled) { src.push(" worldPosition.xyz = worldPosition.xyz + offset;"); } src.push("vec4 viewPosition = viewMatrix * worldPosition; "); src.push("vColor = vec4(float(color.r) / 255.0, float(color.g) / 255.0, float(color.b) / 255.0, float(color.a) / 255.0);"); if (clipping) { src.push("vWorldPosition = worldPosition;"); src.push("vFlags = flags;"); } src.push("vec4 clipPos = projMatrix * viewPosition;"); if (scene.logarithmicDepthBufferEnabled) { src.push("vFragDepth = 1.0 + clipPos.w;"); } src.push("gl_Position = clipPos;"); src.push("}"); src.push("}"); return src; } _buildFragmentShader() { const scene = this._scene; const sectionPlanesState = scene._sectionPlanesState; let i; let len; const clipping = sectionPlanesState.getNumAllocatedSectionPlanes() > 0; const src = []; src.push('#version 300 es'); src.push("// Lines instancing color fragment shader"); src.push("#ifdef GL_FRAGMENT_PRECISION_HIGH"); src.push("precision highp float;"); src.push("precision highp int;"); src.push("#else"); src.push("precision mediump float;"); src.push("precision mediump int;"); src.push("#endif"); if (scene.logarithmicDepthBufferEnabled) { src.push("uniform float logDepthBufFC;"); src.push("in float vFragDepth;"); } if (clipping) { src.push("in vec4 vWorldPosition;"); src.push("in float vFlags;"); for (i = 0, len = sectionPlanesState.getNumAllocatedSectionPlanes(); i < len; i++) { src.push("uniform bool sectionPlaneActive" + i + ";"); src.push("uniform vec3 sectionPlanePos" + i + ";"); src.push("uniform vec3 sectionPlaneDir" + i + ";"); } } src.push("in vec4 vColor;"); src.push("out vec4 outColor;"); src.push("void main(void) {"); if (clipping) { src.push(" bool clippable = (int(vFlags) >> 16 & 0xF) == 1;"); src.push(" if (clippable) {"); src.push(" float dist = 0.0;"); for (i = 0, len = sectionPlanesState.getNumAllocatedSectionPlanes(); i < len; i++) { src.push("if (sectionPlaneActive" + i + ") {"); src.push(" dist += clamp(dot(-sectionPlaneDir" + i + ".xyz, vWorldPosition.xyz - sectionPlanePos" + i + ".xyz), 0.0, 1000.0);"); src.push("}"); } src.push("if (dist > 0.0) { discard; }"); src.push("}"); } // Doing SAO blend in the main solid fill draw shader just so that edge lines can be drawn over the top // Would be more efficient to defer this, then render lines later, using same depth buffer for Z-reject if (this._withSAO) { src.push(" float viewportWidth = uSAOParams[0];"); src.push(" float viewportHeight = uSAOParams[1];"); src.push(" float blendCutoff = uSAOParams[2];"); src.push(" float blendFactor = uSAOParams[3];"); src.push(" vec2 uv = vec2(gl_FragCoord.x / viewportWidth, gl_FragCoord.y / viewportHeight);"); src.push(" float ambient = smoothstep(blendCutoff, 1.0, unpackRGBAToDepth(texture(uOcclusionTexture, uv))) * blendFactor;"); src.push(" outColor = vec4(vColor.rgb * ambient, vColor.a);"); } else { src.push(" outColor = vColor;"); } if (scene.logarithmicDepthBufferEnabled) { src.push("gl_FragDepth = log2( vFragDepth ) * logDepthBufFC * 0.5;"); } src.push("}"); return src; } } /** * @private */ class VBOInstancingLinesSilhouetteRenderer extends VBOInstancingLinesRenderer { drawLayer(frameCtx, instancingLayer, renderPass) { super.drawLayer(frameCtx, instancingLayer, renderPass, { colorUniform: true }); } _buildVertexShader() { const scene = this._scene; const sectionPlanesState = scene._sectionPlanesState; const clipping = sectionPlanesState.getNumAllocatedSectionPlanes() > 0; const src = []; src.push('#version 300 es'); src.push("// Lines instancing silhouette vertex shader"); src.push("uniform int renderPass;"); src.push("in vec3 position;"); if (scene.entityOffsetsEnabled) { src.push("in vec3 offset;"); } src.push("in float flags;"); src.push("in vec4 modelMatrixCol0;"); // Modeling matrix src.push("in vec4 modelMatrixCol1;"); src.push("in vec4 modelMatrixCol2;"); this._addMatricesUniformBlockLines(src); if (scene.logarithmicDepthBufferEnabled) { src.push("uniform float logDepthBufFC;"); src.push("out float vFragDepth;"); } src.push("uniform vec4 color;"); if (clipping) { src.push("out vec4 vWorldPosition;"); src.push("out float vFlags;"); } src.push("void main(void) {"); // silhouetteFlag = NOT_RENDERED | SILHOUETTE_HIGHLIGHTED | SILHOUETTE_SELECTED | | SILHOUETTE_XRAYED // renderPass = SILHOUETTE_HIGHLIGHTED | SILHOUETTE_SELECTED | | SILHOUETTE_XRAYED src.push(`int silhouetteFlag = int(flags) >> 4 & 0xF;`); src.push(`if (silhouetteFlag != renderPass) {`); src.push(" gl_Position = vec4(0.0, 0.0, 0.0, 0.0);"); // Cull vertex src.push("} else {"); src.push("vec4 worldPosition = positionsDecodeMatrix * vec4(position, 1.0); "); src.push("worldPosition = worldMatrix * vec4(dot(worldPosition, modelMatrixCol0), dot(worldPosition, modelMatrixCol1), dot(worldPosition, modelMatrixCol2), 1.0);"); if (scene.entityOffsetsEnabled) { src.push(" worldPosition.xyz = worldPosition.xyz + offset;"); } src.push("vec4 viewPosition = viewMatrix * worldPosition; "); if (clipping) { src.push("vWorldPosition = worldPosition;"); src.push("vFlags = flags;"); } src.push("vec4 clipPos = projMatrix * viewPosition;"); if (scene.logarithmicDepthBufferEnabled) { src.push("vFragDepth = 1.0 + clipPos.w;"); } src.push("gl_Position = clipPos;"); src.push("}"); src.push("}"); return src; } _buildFragmentShader() { const scene = this._scene; const sectionPlanesState = scene._sectionPlanesState; const clipping = sectionPlanesState.getNumAllocatedSectionPlanes() > 0; const src = []; src.push('#version 300 es'); src.push("// Lines instancing silhouette fragment shader"); src.push("#ifdef GL_FRAGMENT_PRECISION_HIGH"); src.push("precision highp float;"); src.push("precision highp int;"); src.push("#else"); src.push("precision mediump float;"); src.push("precision mediump int;"); src.push("#endif"); if (scene.logarithmicDepthBufferEnabled) { src.push("uniform float logDepthBufFC;"); src.push("in float vFragDepth;"); } if (clipping) { src.push("in vec4 vWorldPosition;"); src.push("in float vFlags;"); for (let i = 0, len = sectionPlanesState.getNumAllocatedSectionPlanes(); i < len; i++) { src.push("uniform bool sectionPlaneActive" + i + ";"); src.push("uniform vec3 sectionPlanePos" + i + ";"); src.push("uniform vec3 sectionPlaneDir" + i + ";"); } } src.push("uniform vec4 color;"); src.push("out vec4 outColor;"); src.push("void main(void) {"); if (clipping) { src.push(" bool clippable = (int(vFlags) >> 16 & 0xF) == 1;"); src.push(" if (clippable) {"); src.push(" float dist = 0.0;"); for (let i = 0, len = sectionPlanesState.getNumAllocatedSectionPlanes(); i < len; i++) { src.push("if (sectionPlaneActive" + i + ") {"); src.push(" dist += clamp(dot(-sectionPlaneDir" + i + ".xyz, vWorldPosition.xyz - sectionPlanePos" + i + ".xyz), 0.0, 1000.0);"); src.push("}"); } src.push("if (dist > 0.0) { discard; }"); src.push("}"); } if (scene.logarithmicDepthBufferEnabled) { src.push("gl_FragDepth = log2( vFragDepth ) * logDepthBufFC * 0.5;"); } src.push("outColor = color;"); src.push("}"); return src; } } const tempVec3a$w = math.vec3(); const tempVec3b$s = math.vec3(); const tempVec3c$n = math.vec3(); math.vec3(); const tempMat4a$k = math.mat4(); /** * @private */ class VBOInstancingLinesSnapInitRenderer extends VBORenderer { constructor(scene) { super(scene, false, { instancing: true }); } drawLayer(frameCtx, instancingLayer, renderPass) { if (!this._program) { this._allocate(); if (this.errors) { return; } } if (frameCtx.lastProgramId !== this._program.id) { frameCtx.lastProgramId = this._program.id; this._bindProgram(); } const model = instancingLayer.model; const scene = model.scene; const gl = scene.canvas.gl; const camera = scene.camera; const state = instancingLayer._state; const origin = instancingLayer._state.origin; const {position, rotationMatrix} = model; const aabb = instancingLayer.aabb; // Per-layer AABB for best RTC accuracy const viewMatrix = frameCtx.pickViewMatrix || camera.viewMatrix; if (this._vaoCache.has(instancingLayer)) { gl.bindVertexArray(this._vaoCache.get(instancingLayer)); } else { this._vaoCache.set(instancingLayer, this._makeVAO(state)); } const coordinateScaler = tempVec3a$w; coordinateScaler[0] = math.safeInv(aabb[3] - aabb[0]) * math.MAX_INT; coordinateScaler[1] = math.safeInv(aabb[4] - aabb[1]) * math.MAX_INT; coordinateScaler[2] = math.safeInv(aabb[5] - aabb[2]) * math.MAX_INT; frameCtx.snapPickCoordinateScale[0] = math.safeInv(coordinateScaler[0]); frameCtx.snapPickCoordinateScale[1] = math.safeInv(coordinateScaler[1]); frameCtx.snapPickCoordinateScale[2] = math.safeInv(coordinateScaler[2]); let rtcViewMatrix; if (origin || position[0] !== 0 || position[1] !== 0 || position[2] !== 0) { const rtcOrigin = tempVec3b$s; if (origin) { const rotatedOrigin = math.transformPoint3(rotationMatrix, origin, tempVec3c$n); rtcOrigin[0] = rotatedOrigin[0]; rtcOrigin[1] = rotatedOrigin[1]; rtcOrigin[2] = rotatedOrigin[2]; } else { rtcOrigin[0] = 0; rtcOrigin[1] = 0; rtcOrigin[2] = 0; } rtcOrigin[0] += position[0]; rtcOrigin[1] += position[1]; rtcOrigin[2] += position[2]; rtcViewMatrix = createRTCViewMat(viewMatrix, rtcOrigin, tempMat4a$k); frameCtx.snapPickOrigin[0] = rtcOrigin[0]; frameCtx.snapPickOrigin[1] = rtcOrigin[1]; frameCtx.snapPickOrigin[2] = rtcOrigin[2]; } else { rtcViewMatrix = viewMatrix; frameCtx.snapPickOrigin[0] = 0; frameCtx.snapPickOrigin[1] = 0; frameCtx.snapPickOrigin[2] = 0; } gl.uniform2fv(this.uVectorA, frameCtx.snapVectorA); gl.uniform2fv(this.uInverseVectorAB, frameCtx.snapInvVectorAB); gl.uniform1i(this._uLayerNumber, frameCtx.snapPickLayerNumber); gl.uniform3fv(this._uCoordinateScaler, coordinateScaler); gl.uniform1i(this._uRenderPass, renderPass); gl.uniform1i(this._uPickInvisible, frameCtx.pickInvisible); let offset = 0; const mat4Size = 4 * 4; this._matricesUniformBlockBufferData.set(rotationMatrix, 0); this._matricesUniformBlockBufferData.set(rtcViewMatrix, offset += mat4Size); this._matricesUniformBlockBufferData.set(camera.projMatrix, offset += mat4Size); this._matricesUniformBlockBufferData.set(state.positionsDecodeMatrix, offset += mat4Size); gl.bindBuffer(gl.UNIFORM_BUFFER, this._matricesUniformBlockBuffer); gl.bufferData(gl.UNIFORM_BUFFER, this._matricesUniformBlockBufferData, gl.DYNAMIC_DRAW); gl.bindBufferBase( gl.UNIFORM_BUFFER, this._matricesUniformBlockBufferBindingPoint, this._matricesUniformBlockBuffer); { const logDepthBufFC = 2.0 / (Math.log(frameCtx.pickZFar + 1.0) / Math.LN2); gl.uniform1f(this._uLogDepthBufFC, logDepthBufFC); } this.setSectionPlanesStateUniforms(instancingLayer); state.indicesBuf.bind(); gl.drawElementsInstanced(gl.LINES, state.indicesBuf.numItems, state.indicesBuf.itemType, 0, state.numInstances); state.indicesBuf.unbind(); gl.bindVertexArray(null); } _allocate() { super._allocate(); const program = this._program; { this._uLogDepthBufFC = program.getLocation("logDepthBufFC"); } this.uVectorA = program.getLocation("snapVectorA"); this.uInverseVectorAB = program.getLocation("snapInvVectorAB"); this._uLayerNumber = program.getLocation("layerNumber"); this._uCoordinateScaler = program.getLocation("coordinateScaler"); } _bindProgram() { this._program.bind(); } _buildVertexShader() { const scene = this._scene; const sectionPlanesState = scene._sectionPlanesState; const clipping = sectionPlanesState.getNumAllocatedSectionPlanes() > 0; const src = []; src.push ('#version 300 es'); src.push("// SnapInstancingDepthBufInitRenderer vertex shader"); src.push("#ifdef GL_FRAGMENT_PRECISION_HIGH"); src.push("precision highp float;"); src.push("precision highp int;"); src.push("precision highp usampler2D;"); src.push("precision highp isampler2D;"); src.push("precision highp sampler2D;"); src.push("#else"); src.push("precision mediump float;"); src.push("precision mediump int;"); src.push("precision mediump usampler2D;"); src.push("precision mediump isampler2D;"); src.push("precision mediump sampler2D;"); src.push("#endif"); src.push("uniform int renderPass;"); src.push("in vec4 pickColor;"); src.push("in vec3 position;"); if (scene.entityOffsetsEnabled) { src.push("in vec3 offset;"); } src.push("in float flags;"); src.push("in vec4 modelMatrixCol0;"); // Modeling matrix src.push("in vec4 modelMatrixCol1;"); src.push("in vec4 modelMatrixCol2;"); src.push("uniform bool pickInvisible;"); this._addMatricesUniformBlockLines(src); src.push("uniform vec2 snapVectorA;"); src.push("uniform vec2 snapInvVectorAB;"); { src.push("uniform float logDepthBufFC;"); src.push("out float vFragDepth;"); src.push("bool isPerspectiveMatrix(mat4 m) {"); src.push(" return (m[2][3] == - 1.0);"); src.push("}"); src.push("out float isPerspective;"); } src.push("vec2 remapClipPos(vec2 clipPos) {"); src.push(" float x = (clipPos.x - snapVectorA.x) * snapInvVectorAB.x;"); src.push(" float y = (clipPos.y - snapVectorA.y) * snapInvVectorAB.y;"); src.push(" return vec2(x, y);"); src.push("}"); src.push("flat out vec4 vPickColor;"); src.push("out vec4 vWorldPosition;"); if (clipping) { src.push("out float vFlags;"); } src.push("out highp vec3 relativeToOriginPosition;"); src.push("void main(void) {"); // pickFlag = NOT_RENDERED | PICK // renderPass = PICK src.push(`int pickFlag = int(flags) >> 12 & 0xF;`); src.push(`if (pickFlag != renderPass) {`); src.push(" gl_Position = vec4(0.0, 0.0, 0.0, 0.0);"); // Cull vertex src.push("} else {"); src.push(" vec4 worldPosition = positionsDecodeMatrix * vec4(position, 1.0); "); src.push(" worldPosition = worldMatrix * vec4(dot(worldPosition, modelMatrixCol0), dot(worldPosition, modelMatrixCol1), dot(worldPosition, modelMatrixCol2), 1.0);"); if (scene.entityOffsetsEnabled) { src.push(" worldPosition.xyz = worldPosition.xyz + offset;"); } src.push("relativeToOriginPosition = worldPosition.xyz;"); src.push(" vec4 viewPosition = viewMatrix * worldPosition; "); src.push(" vWorldPosition = worldPosition;"); if (clipping) { src.push(" vFlags = flags;"); } src.push("vPickColor = pickColor;"); src.push("vec4 clipPos = projMatrix * viewPosition;"); src.push("float tmp = clipPos.w;"); src.push("clipPos.xyzw /= tmp;"); src.push("clipPos.xy = remapClipPos(clipPos.xy);"); src.push("clipPos.xyzw *= tmp;"); { src.push("vFragDepth = 1.0 + clipPos.w;"); src.push("isPerspective = float (isPerspectiveMatrix(projMatrix));"); } src.push("gl_Position = clipPos;"); src.push("}"); src.push("}"); return src; } _buildFragmentShader() { const scene = this._scene; const sectionPlanesState = scene._sectionPlanesState; const clipping = sectionPlanesState.getNumAllocatedSectionPlanes() > 0; const src = []; src.push ('#version 300 es'); src.push("// Points instancing pick depth fragment shader"); src.push("#ifdef GL_FRAGMENT_PRECISION_HIGH"); src.push("precision highp float;"); src.push("precision highp int;"); src.push("#else"); src.push("precision mediump float;"); src.push("precision mediump int;"); src.push("#endif"); { src.push("in float isPerspective;"); src.push("uniform float logDepthBufFC;"); src.push("in float vFragDepth;"); } src.push("uniform int layerNumber;"); src.push("uniform vec3 coordinateScaler;"); src.push("in vec4 vWorldPosition;"); src.push("flat in vec4 vPickColor;"); if (clipping) { src.push("in float vFlags;"); for (let i = 0; i < sectionPlanesState.getNumAllocatedSectionPlanes(); i++) { src.push("uniform bool sectionPlaneActive" + i + ";"); src.push("uniform vec3 sectionPlanePos" + i + ";"); src.push("uniform vec3 sectionPlaneDir" + i + ";"); } } src.push("in highp vec3 relativeToOriginPosition;"); src.push("layout(location = 0) out highp ivec4 outCoords;"); /////////////////////////////////////////// // TODO: normal placeholder? // Primitive type? /////////////////////////////////////////// src.push("layout(location = 2) out lowp uvec4 outPickColor;"); src.push("void main(void) {"); if (clipping) { src.push(" bool clippable = (int(vFlags) >> 16 & 0xF) == 1;"); src.push(" if (clippable) {"); src.push(" float dist = 0.0;"); for (let i = 0; i < sectionPlanesState.getNumAllocatedSectionPlanes(); i++) { src.push("if (sectionPlaneActive" + i + ") {"); src.push(" dist += clamp(dot(-sectionPlaneDir" + i + ".xyz, vWorldPosition.xyz - sectionPlanePos" + i + ".xyz), 0.0, 1000.0);"); src.push("}"); } src.push("if (dist > 0.0) { discard; }"); src.push("}"); } { src.push(" float dx = dFdx(vFragDepth);"); src.push(" float dy = dFdy(vFragDepth);"); src.push(" float diff = sqrt(dx*dx+dy*dy);"); src.push(" gl_FragDepth = isPerspective == 0.0 ? gl_FragCoord.z : log2( vFragDepth + diff ) * logDepthBufFC * 0.5;"); } src.push("outCoords = ivec4(relativeToOriginPosition.xyz*coordinateScaler.xyz, -layerNumber);"); src.push("outPickColor = uvec4(vPickColor);"); src.push("}"); return src; } webglContextRestored() { this._program = null; } destroy() { if (this._program) { this._program.destroy(); } this._program = null; } } const tempVec3a$v = math.vec3(); const tempVec3b$r = math.vec3(); const tempVec3c$m = math.vec3(); math.vec3(); const tempMat4a$j = math.mat4(); /** * @private */ class VBOInstancingLinesSnapRenderer extends VBORenderer { constructor(scene) { super(scene, false, { instancing: true }); } drawLayer(frameCtx, instancingLayer, renderPass) { if (!this._program) { this._allocate(instancingLayer); if (this.errors) { return; } } if (frameCtx.lastProgramId !== this._program.id) { frameCtx.lastProgramId = this._program.id; this._bindProgram(); } const model = instancingLayer.model; const scene = model.scene; const camera = scene.camera; const gl = scene.canvas.gl; const state = instancingLayer._state; const origin = instancingLayer._state.origin; const {position, rotationMatrix} = model; const aabb = instancingLayer.aabb; // Per-layer AABB for best RTC accuracy const viewMatrix = frameCtx.pickViewMatrix || camera.viewMatrix; if (this._vaoCache.has(instancingLayer)) { gl.bindVertexArray(this._vaoCache.get(instancingLayer)); } else { this._vaoCache.set(instancingLayer, this._makeVAO(state)); } const coordinateScaler = tempVec3a$v; coordinateScaler[0] = math.safeInv(aabb[3] - aabb[0]) * math.MAX_INT; coordinateScaler[1] = math.safeInv(aabb[4] - aabb[1]) * math.MAX_INT; coordinateScaler[2] = math.safeInv(aabb[5] - aabb[2]) * math.MAX_INT; frameCtx.snapPickCoordinateScale[0] = math.safeInv(coordinateScaler[0]); frameCtx.snapPickCoordinateScale[1] = math.safeInv(coordinateScaler[1]); frameCtx.snapPickCoordinateScale[2] = math.safeInv(coordinateScaler[2]); let rtcViewMatrix; if (origin || position[0] !== 0 || position[1] !== 0 || position[2] !== 0) { const rtcOrigin = tempVec3b$r; if (origin) { const rotatedOrigin = math.transformPoint3(rotationMatrix, origin, tempVec3c$m); rtcOrigin[0] = rotatedOrigin[0]; rtcOrigin[1] = rotatedOrigin[1]; rtcOrigin[2] = rotatedOrigin[2]; } else { rtcOrigin[0] = 0; rtcOrigin[1] = 0; rtcOrigin[2] = 0; } rtcOrigin[0] += position[0]; rtcOrigin[1] += position[1]; rtcOrigin[2] += position[2]; rtcViewMatrix = createRTCViewMat(viewMatrix, rtcOrigin, tempMat4a$j); frameCtx.snapPickOrigin[0] = rtcOrigin[0]; frameCtx.snapPickOrigin[1] = rtcOrigin[1]; frameCtx.snapPickOrigin[2] = rtcOrigin[2]; } else { rtcViewMatrix = viewMatrix; frameCtx.snapPickOrigin[0] = 0; frameCtx.snapPickOrigin[1] = 0; frameCtx.snapPickOrigin[2] = 0; } gl.uniform2fv(this.uVectorA, frameCtx.snapVectorA); gl.uniform2fv(this.uInverseVectorAB, frameCtx.snapInvVectorAB); gl.uniform1i(this._uLayerNumber, frameCtx.snapPickLayerNumber); gl.uniform3fv(this._uCoordinateScaler, coordinateScaler); gl.uniform1i(this._uRenderPass, renderPass); gl.uniform1i(this._uPickInvisible, frameCtx.pickInvisible); let offset = 0; const mat4Size = 4 * 4; this._matricesUniformBlockBufferData.set(rotationMatrix, 0); this._matricesUniformBlockBufferData.set(rtcViewMatrix, offset += mat4Size); this._matricesUniformBlockBufferData.set(camera.projMatrix, offset += mat4Size); this._matricesUniformBlockBufferData.set(state.positionsDecodeMatrix, offset += mat4Size); gl.bindBuffer(gl.UNIFORM_BUFFER, this._matricesUniformBlockBuffer); gl.bufferData(gl.UNIFORM_BUFFER, this._matricesUniformBlockBufferData, gl.DYNAMIC_DRAW); gl.bindBufferBase( gl.UNIFORM_BUFFER, this._matricesUniformBlockBufferBindingPoint, this._matricesUniformBlockBuffer); { const logDepthBufFC = 2.0 / (Math.log(frameCtx.pickZFar + 1.0) / Math.LN2); // TODO: Far from pick project matrix gl.uniform1f(this._uLogDepthBufFC, logDepthBufFC); } this.setSectionPlanesStateUniforms(instancingLayer); if (frameCtx.snapMode === "edge") { state.indicesBuf.bind(); gl.drawElementsInstanced(gl.LINES, state.indicesBuf.numItems, state.indicesBuf.itemType, 0, state.numInstances); state.indicesBuf.unbind(); // needed? } else { gl.drawArraysInstanced(gl.POINTS, 0, state.positionsBuf.numItems, state.numInstances); } gl.bindVertexArray(null); } _allocate() { super._allocate(); const program = this._program; { this._uLogDepthBufFC = program.getLocation("logDepthBufFC"); } this.uVectorA = program.getLocation("snapVectorA"); this.uInverseVectorAB = program.getLocation("snapInvVectorAB"); this._uLayerNumber = program.getLocation("layerNumber"); this._uCoordinateScaler = program.getLocation("coordinateScaler"); } _bindProgram() { this._program.bind(); } _buildVertexShader() { const scene = this._scene; const sectionPlanesState = scene._sectionPlanesState; const clipping = sectionPlanesState.getNumAllocatedSectionPlanes() > 0; const src = []; src.push ('#version 300 es'); src.push("// SnapInstancingDepthRenderer vertex shader"); src.push("#ifdef GL_FRAGMENT_PRECISION_HIGH"); src.push("precision highp float;"); src.push("precision highp int;"); src.push("precision highp usampler2D;"); src.push("precision highp isampler2D;"); src.push("precision highp sampler2D;"); src.push("#else"); src.push("precision mediump float;"); src.push("precision mediump int;"); src.push("precision mediump usampler2D;"); src.push("precision mediump isampler2D;"); src.push("precision mediump sampler2D;"); src.push("#endif"); src.push("uniform int renderPass;"); src.push("in vec3 position;"); if (scene.entityOffsetsEnabled) { src.push("in vec3 offset;"); } src.push("in float flags;"); src.push("in vec4 modelMatrixCol0;"); // Modeling matrix src.push("in vec4 modelMatrixCol1;"); src.push("in vec4 modelMatrixCol2;"); src.push("uniform bool pickInvisible;"); this._addMatricesUniformBlockLines(src); src.push("uniform vec2 snapVectorA;"); src.push("uniform vec2 snapInvVectorAB;"); { src.push("uniform float logDepthBufFC;"); src.push("out float vFragDepth;"); src.push("bool isPerspectiveMatrix(mat4 m) {"); src.push(" return (m[2][3] == - 1.0);"); src.push("}"); src.push("out float isPerspective;"); } src.push("vec2 remapClipPos(vec2 clipPos) {"); src.push(" float x = (clipPos.x - snapVectorA.x) * snapInvVectorAB.x;"); src.push(" float y = (clipPos.y - snapVectorA.y) * snapInvVectorAB.y;"); src.push(" return vec2(x, y);"); src.push("}"); if (clipping) { src.push("out vec4 vWorldPosition;"); src.push("out float vFlags;"); } src.push("out highp vec3 relativeToOriginPosition;"); src.push("void main(void) {"); // pickFlag = NOT_RENDERED | PICK // renderPass = PICK src.push(`int pickFlag = int(flags) >> 12 & 0xF;`); src.push(`if (pickFlag != renderPass) {`); src.push(" gl_Position = vec4(2.0, 0.0, 0.0, 0.0);"); // Cull vertex src.push("} else {"); src.push(" vec4 worldPosition = positionsDecodeMatrix * vec4(position, 1.0); "); src.push(" worldPosition = worldMatrix * vec4(dot(worldPosition, modelMatrixCol0), dot(worldPosition, modelMatrixCol1), dot(worldPosition, modelMatrixCol2), 1.0);"); if (scene.entityOffsetsEnabled) { src.push(" worldPosition.xyz = worldPosition.xyz + offset;"); } src.push("relativeToOriginPosition = worldPosition.xyz;"); src.push(" vec4 viewPosition = viewMatrix * worldPosition; "); if (clipping) { src.push(" vWorldPosition = worldPosition;"); src.push(" vFlags = flags;"); } src.push("vec4 clipPos = projMatrix * viewPosition;"); src.push("float tmp = clipPos.w;"); src.push("clipPos.xyzw /= tmp;"); src.push("clipPos.xy = remapClipPos(clipPos.xy);"); src.push("clipPos.xyzw *= tmp;"); { src.push("vFragDepth = 1.0 + clipPos.w;"); src.push("isPerspective = float (isPerspectiveMatrix(projMatrix));"); } src.push("gl_Position = clipPos;"); src.push("gl_PointSize = 1.0;"); // Windows needs this? src.push("}"); src.push("}"); return src; } _buildFragmentShader() { const scene = this._scene; const sectionPlanesState = scene._sectionPlanesState; const clipping = sectionPlanesState.getNumAllocatedSectionPlanes() > 0; const src = []; src.push ('#version 300 es'); src.push("// SnapInstancingDepthRenderer fragment shader"); src.push("#ifdef GL_FRAGMENT_PRECISION_HIGH"); src.push("precision highp float;"); src.push("precision highp int;"); src.push("#else"); src.push("precision mediump float;"); src.push("precision mediump int;"); src.push("#endif"); { src.push("in float isPerspective;"); src.push("uniform float logDepthBufFC;"); src.push("in float vFragDepth;"); } src.push("uniform int layerNumber;"); src.push("uniform vec3 coordinateScaler;"); if (clipping) { src.push("in vec4 vWorldPosition;"); src.push("in float vFlags;"); for (let i = 0; i < sectionPlanesState.getNumAllocatedSectionPlanes(); i++) { src.push("uniform bool sectionPlaneActive" + i + ";"); src.push("uniform vec3 sectionPlanePos" + i + ";"); src.push("uniform vec3 sectionPlaneDir" + i + ";"); } } src.push("in highp vec3 relativeToOriginPosition;"); src.push("out highp ivec4 outCoords;"); src.push("void main(void) {"); if (clipping) { src.push(" bool clippable = (int(vFlags) >> 16 & 0xF) == 1;"); src.push(" if (clippable) {"); src.push(" float dist = 0.0;"); for (let i = 0; i < sectionPlanesState.getNumAllocatedSectionPlanes(); i++) { src.push("if (sectionPlaneActive" + i + ") {"); src.push(" dist += clamp(dot(-sectionPlaneDir" + i + ".xyz, vWorldPosition.xyz - sectionPlanePos" + i + ".xyz), 0.0, 1000.0);"); src.push("}"); } src.push("if (dist > 0.0) { discard; }"); src.push("}"); } { src.push(" gl_FragDepth = isPerspective == 0.0 ? gl_FragCoord.z : log2( vFragDepth ) * logDepthBufFC * 0.5;"); } src.push("outCoords = ivec4(relativeToOriginPosition.xyz*coordinateScaler.xyz, layerNumber);"); src.push("}"); return src; } webglContextRestored() { this._program = null; } destroy() { if (this._program) { this._program.destroy(); } this._program = null; } } /** * @private */ class VBOInstancingLinesRenderers { constructor(scene) { this._scene = scene; } _compile() { if (this._colorRenderer && (!this._colorRenderer.getValid())) { this._colorRenderer.destroy(); this._colorRenderer = null; } if (this._silhouetteRenderer && (!this._silhouetteRenderer.getValid())) { this._silhouetteRenderer.destroy(); this._silhouetteRenderer = null; } if (this._snapInitRenderer && (!this._snapInitRenderer.getValid())) { this._snapInitRenderer.destroy(); this._snapInitRenderer = null; } if (this._snapRenderer && (!this._snapRenderer.getValid())) { this._snapRenderer.destroy(); this._snapRenderer = null; } } eagerCreateRenders() { // Pre-initialize renderers that would otherwise be lazy-initialised // on user interaction, such as picking or emphasis, so that there is no delay // when user first begins interacting with the viewer. if (!this._snapInitRenderer) { this._snapInitRenderer = new VBOInstancingLinesSnapInitRenderer(this._scene, false); } if (!this._snapRenderer) { this._snapRenderer = new VBOInstancingLinesSnapRenderer(this._scene); } } get colorRenderer() { if (!this._colorRenderer) { this._colorRenderer = new VBOInstancingLinesColorRenderer(this._scene); } return this._colorRenderer; } get silhouetteRenderer() { if (!this._silhouetteRenderer) { this._silhouetteRenderer = new VBOInstancingLinesSilhouetteRenderer(this._scene); } return this._silhouetteRenderer; } get snapInitRenderer() { if (!this._snapInitRenderer) { this._snapInitRenderer = new VBOInstancingLinesSnapInitRenderer(this._scene, false); } return this._snapInitRenderer; } get snapRenderer() { if (!this._snapRenderer) { this._snapRenderer = new VBOInstancingLinesSnapRenderer(this._scene); } return this._snapRenderer; } _destroy() { if (this._colorRenderer) { this._colorRenderer.destroy(); } if (this._silhouetteRenderer) { this._silhouetteRenderer.destroy(); } if (this._snapInitRenderer) { this._snapInitRenderer.destroy(); } if (this._snapRenderer) { this._snapRenderer.destroy(); } } } const cachedRenderers$3 = {}; /** * @private */ function getRenderers$4(scene) { const sceneId = scene.id; let instancingRenderers = cachedRenderers$3[sceneId]; if (!instancingRenderers) { instancingRenderers = new VBOInstancingLinesRenderers(scene); cachedRenderers$3[sceneId] = instancingRenderers; instancingRenderers._compile(); scene.on("compile", () => { instancingRenderers._compile(); }); scene.on("destroyed", () => { delete cachedRenderers$3[sceneId]; instancingRenderers._destroy(); }); } return instancingRenderers; } /* function getSnapInstancingRenderers(scene) { const sceneId = scene.id; let instancingRenderers = cachedRenderers[sceneId]; if (!instancingRenderers) { instancingRenderers = new VBOInstancingLineSnapRenderers(scene); cachedRenderers[sceneId] = instancingRenderers; instancingRenderers._compile(); instancingRenderers.eagerCreateRenders(); scene.on("compile", () => { instancingRenderers._compile(); instancingRenderers.eagerCreateRenders(); }); scene.on("destroyed", () => { delete cachedRenderers[sceneId]; instancingRenderers._destroy(); }); } return instancingRenderers; } */ const tempUint8Vec4$1 = new Uint8Array(4); const tempFloat32$1 = new Float32Array(1); const tempVec3fa$1 = new Float32Array(3); const tempFloat32Vec4$1 = new Float32Array(4); /** * @private */ class VBOInstancingLinesLayer { /** * @param cfg * @param cfg.layerIndex * @param cfg.model * @param cfg.geometry * @param cfg.material * @param cfg.origin */ constructor(cfg) { // console.info("VBOInstancingLinesLayer"); /** * Owner model * @type {VBOSceneModel} */ this.model = cfg.model; /** * Shared material * @type {VBOSceneModelGeometry} */ this.material = cfg.material; /** * State sorting key. * @type {string} */ this.sortId = "LinesInstancingLayer"; /** * Index of this InstancingLayer in VBOSceneModel#_layerList * @type {Number} */ this.layerIndex = cfg.layerIndex; this._renderers = getRenderers$4(cfg.model.scene); this._aabb = math.collapseAABB3(); this._state = new RenderState({ obb: math.OBB3(), numInstances: 0, origin: null, geometry: cfg.geometry, positionsDecodeMatrix: cfg.geometry.positionsDecodeMatrix, // So we can null the geometry for GC positionsBuf: null, colorsBuf: null, flagsBuf: null, offsetsBuf: null, modelMatrixCol0Buf: null, modelMatrixCol1Buf: null, modelMatrixCol2Buf: null }); // These counts are used to avoid unnecessary render passes this._numPortions = 0; this._numVisibleLayerPortions = 0; this._numTransparentLayerPortions = 0; this._numXRayedLayerPortions = 0; this._numHighlightedLayerPortions = 0; this._numSelectedLayerPortions = 0; this._numClippableLayerPortions = 0; this._numEdgesLayerPortions = 0; this._numPickableLayerPortions = 0; this._numCulledLayerPortions = 0; /** @private */ this.numIndices = cfg.geometry.numIndices; // Vertex arrays this._colors = []; this._offsets = []; // Modeling matrix per instance, array for each column this._modelMatrixCol0 = []; this._modelMatrixCol1 = []; this._modelMatrixCol2 = []; this._portions = []; this._meshes = []; this._aabb = math.collapseAABB3(); this.aabbDirty = true; if (cfg.origin) { this._state.origin = math.vec3(cfg.origin); } this._finalized = false; /** * The type of primitives in this layer. */ this.primitive = cfg.primitive; } get aabb() { if (this.aabbDirty) { math.collapseAABB3(this._aabb); for (let i = 0, len = this._meshes.length; i < len; i++) { math.expandAABB3(this._aabb, this._meshes[i].aabb); } this.aabbDirty = false; } return this._aabb; } /** * Creates a new portion within this InstancingLayer, returns the new portion ID. * * The portion will instance this InstancingLayer's geometry. * * Gives the portion the specified color and matrix. * * @param mesh The SceneModelMesh that owns the portion * @param cfg Portion params * @param cfg.color Color [0..255,0..255,0..255] * @param cfg.opacity Opacity [0..255]. * @param cfg.meshMatrix Flat float 4x4 matrix. * @returns {number} Portion ID. */ createPortion(mesh, cfg) { const color = cfg.color; const opacity = cfg.opacity; const meshMatrix = cfg.meshMatrix; if (this._finalized) { throw "Already finalized"; } const r = color[0]; // Color is pre-quantized by VBOSceneModel const g = color[1]; const b = color[2]; color[3]; this._colors.push(r); this._colors.push(g); this._colors.push(b); this._colors.push(opacity); if (this.model.scene.entityOffsetsEnabled) { this._offsets.push(0); this._offsets.push(0); this._offsets.push(0); } this._modelMatrixCol0.push(meshMatrix[0]); this._modelMatrixCol0.push(meshMatrix[4]); this._modelMatrixCol0.push(meshMatrix[8]); this._modelMatrixCol0.push(meshMatrix[12]); this._modelMatrixCol1.push(meshMatrix[1]); this._modelMatrixCol1.push(meshMatrix[5]); this._modelMatrixCol1.push(meshMatrix[9]); this._modelMatrixCol1.push(meshMatrix[13]); this._modelMatrixCol2.push(meshMatrix[2]); this._modelMatrixCol2.push(meshMatrix[6]); this._modelMatrixCol2.push(meshMatrix[10]); this._modelMatrixCol2.push(meshMatrix[14]); this._state.numInstances++; const portionId = this._portions.length; this._portions.push({}); this._numPortions++; this.model.numPortions++; this._meshes.push(mesh); return portionId; } finalize() { if (this._finalized) { throw "Already finalized"; } const gl = this.model.scene.canvas.gl; const state = this._state; const geometry = state.geometry; const colorsLength = this._colors.length; const flagsLength = colorsLength / 4; if (colorsLength > 0) { let notNormalized = false; this._state.colorsBuf = new ArrayBuf(gl, gl.ARRAY_BUFFER, new Uint8Array(this._colors), this._colors.length, 4, gl.DYNAMIC_DRAW, notNormalized); this._colors = []; // Release memory } if (flagsLength > 0) { // Because we only build flags arrays here, // get their length from the colors array let notNormalized = false; this._state.flagsBuf = new ArrayBuf(gl, gl.ARRAY_BUFFER, new Float32Array(flagsLength), flagsLength, 1, gl.DYNAMIC_DRAW, notNormalized); } if (this.model.scene.entityOffsetsEnabled) { if (this._offsets.length > 0) { const notNormalized = false; this._state.offsetsBuf = new ArrayBuf(gl, gl.ARRAY_BUFFER, new Float32Array(this._offsets), this._offsets.length, 3, gl.DYNAMIC_DRAW, notNormalized); this._offsets = []; // Release memory } } if (geometry.colorsCompressed && geometry.colorsCompressed.length > 0) { const colorsCompressed = new Uint8Array(geometry.colorsCompressed); const notNormalized = false; state.colorsBuf = new ArrayBuf(gl, gl.ARRAY_BUFFER, colorsCompressed, colorsCompressed.length, 4, gl.STATIC_DRAW, notNormalized); } if (geometry.positionsCompressed && geometry.positionsCompressed.length > 0) { const normalized = false; state.positionsBuf = new ArrayBuf(gl, gl.ARRAY_BUFFER, geometry.positionsCompressed, geometry.positionsCompressed.length, 3, gl.STATIC_DRAW, normalized); state.positionsDecodeMatrix = math.mat4(geometry.positionsDecodeMatrix); } if (geometry.indices && geometry.indices.length > 0) { state.indicesBuf = new ArrayBuf(gl, gl.ELEMENT_ARRAY_BUFFER, new Uint32Array(geometry.indices), geometry.indices.length, 1, gl.STATIC_DRAW); state.numIndices = geometry.indices.length; } if (this._modelMatrixCol0.length > 0) { const normalized = false; this._state.modelMatrixCol0Buf = new ArrayBuf(gl, gl.ARRAY_BUFFER, new Float32Array(this._modelMatrixCol0), this._modelMatrixCol0.length, 4, gl.STATIC_DRAW, normalized); this._state.modelMatrixCol1Buf = new ArrayBuf(gl, gl.ARRAY_BUFFER, new Float32Array(this._modelMatrixCol1), this._modelMatrixCol1.length, 4, gl.STATIC_DRAW, normalized); this._state.modelMatrixCol2Buf = new ArrayBuf(gl, gl.ARRAY_BUFFER, new Float32Array(this._modelMatrixCol2), this._modelMatrixCol2.length, 4, gl.STATIC_DRAW, normalized); this._modelMatrixCol0 = []; this._modelMatrixCol1 = []; this._modelMatrixCol2 = []; } if (!this.model.scene.readableGeometryEnabled) { this._state.geometry = null; } this._finalized = true; } // The following setters are called by VBOSceneModelMesh, in turn called by VBOSceneModelNode, only after the layer is finalized. // It's important that these are called after finalize() in order to maintain integrity of counts like _numVisibleLayerPortions etc. initFlags(portionId, flags, meshTransparent) { if (flags & ENTITY_FLAGS.VISIBLE) { this._numVisibleLayerPortions++; this.model.numVisibleLayerPortions++; } if (flags & ENTITY_FLAGS.HIGHLIGHTED) { this._numHighlightedLayerPortions++; this.model.numHighlightedLayerPortions++; } if (flags & ENTITY_FLAGS.XRAYED) { this._numXRayedLayerPortions++; this.model.numXRayedLayerPortions++; } if (flags & ENTITY_FLAGS.SELECTED) { this._numSelectedLayerPortions++; this.model.numSelectedLayerPortions++; } if (flags & ENTITY_FLAGS.CLIPPABLE) { this._numClippableLayerPortions++; this.model.numClippableLayerPortions++; } if (flags & ENTITY_FLAGS.EDGES) { this._numEdgesLayerPortions++; this.model.numEdgesLayerPortions++; } if (flags & ENTITY_FLAGS.PICKABLE) { this._numPickableLayerPortions++; this.model.numPickableLayerPortions++; } if (flags & ENTITY_FLAGS.CULLED) { this._numCulledLayerPortions++; this.model.numCulledLayerPortions++; } if (meshTransparent) { this._numTransparentLayerPortions++; this.model.numTransparentLayerPortions++; } this._setFlags(portionId, flags, meshTransparent); } setVisible(portionId, flags, meshTransparent) { if (!this._finalized) { throw "Not finalized"; } if (flags & ENTITY_FLAGS.VISIBLE) { this._numVisibleLayerPortions++; this.model.numVisibleLayerPortions++; } else { this._numVisibleLayerPortions--; this.model.numVisibleLayerPortions--; } this._setFlags(portionId, flags, meshTransparent); } setHighlighted(portionId, flags, meshTransparent) { if (!this._finalized) { throw "Not finalized"; } if (flags & ENTITY_FLAGS.HIGHLIGHTED) { this._numHighlightedLayerPortions++; this.model.numHighlightedLayerPortions++; } else { this._numHighlightedLayerPortions--; this.model.numHighlightedLayerPortions--; } this._setFlags(portionId, flags, meshTransparent); } setXRayed(portionId, flags, meshTransparent) { if (!this._finalized) { throw "Not finalized"; } if (flags & ENTITY_FLAGS.XRAYED) { this._numXRayedLayerPortions++; this.model.numXRayedLayerPortions++; } else { this._numXRayedLayerPortions--; this.model.numXRayedLayerPortions--; } this._setFlags(portionId, flags, meshTransparent); } setSelected(portionId, flags, meshTransparent) { if (!this._finalized) { throw "Not finalized"; } if (flags & ENTITY_FLAGS.SELECTED) { this._numSelectedLayerPortions++; this.model.numSelectedLayerPortions++; } else { this._numSelectedLayerPortions--; this.model.numSelectedLayerPortions--; } this._setFlags(portionId, flags, meshTransparent); } setEdges(portionId, flags, meshTransparent) { if (!this._finalized) { throw "Not finalized"; } if (flags & ENTITY_FLAGS.EDGES) { this._numEdgesLayerPortions++; this.model.numEdgesLayerPortions++; } else { this._numEdgesLayerPortions--; this.model.numEdgesLayerPortions--; } this._setFlags(portionId, flags, meshTransparent); } setClippable(portionId, flags) { if (!this._finalized) { throw "Not finalized"; } if (flags & ENTITY_FLAGS.CLIPPABLE) { this._numClippableLayerPortions++; this.model.numClippableLayerPortions++; } else { this._numClippableLayerPortions--; this.model.numClippableLayerPortions--; } this._setFlags(portionId, flags); } setCollidable(portionId, flags) { if (!this._finalized) { throw "Not finalized"; } } setPickable(portionId, flags, meshTransparent) { if (!this._finalized) { throw "Not finalized"; } if (flags & ENTITY_FLAGS.PICKABLE) { this._numPickableLayerPortions++; this.model.numPickableLayerPortions++; } else { this._numPickableLayerPortions--; this.model.numPickableLayerPortions--; } this._setFlags(portionId, flags, meshTransparent); } setCulled(portionId, flags, meshTransparent) { if (!this._finalized) { throw "Not finalized"; } if (flags & ENTITY_FLAGS.CULLED) { this._numCulledLayerPortions++; this.model.numCulledLayerPortions++; } else { this._numCulledLayerPortions--; this.model.numCulledLayerPortions--; } this._setFlags(portionId, flags, meshTransparent); } setColor(portionId, color) { // RGBA color is normalized as ints if (!this._finalized) { throw "Not finalized"; } tempUint8Vec4$1[0] = color[0]; tempUint8Vec4$1[1] = color[1]; tempUint8Vec4$1[2] = color[2]; tempUint8Vec4$1[3] = color[3]; this._state.colorsBuf.setData(tempUint8Vec4$1, portionId * 4, 4); } setTransparent(portionId, flags, transparent) { if (transparent) { this._numTransparentLayerPortions++; this.model.numTransparentLayerPortions++; } else { this._numTransparentLayerPortions--; this.model.numTransparentLayerPortions--; } this._setFlags(portionId, flags, transparent); } /** * flags are 4bits values encoded on a 32bit base. color flag on the first 4 bits, silhouette flag on the next 4 bits and so on for edge, pick and clippable. */ _setFlags(portionId, flags, meshTransparent) { if (!this._finalized) { throw "Not finalized"; } const visible = !!(flags & ENTITY_FLAGS.VISIBLE); const xrayed = !!(flags & ENTITY_FLAGS.XRAYED); const highlighted = !!(flags & ENTITY_FLAGS.HIGHLIGHTED); const selected = !!(flags & ENTITY_FLAGS.SELECTED); const edges = !!(flags & ENTITY_FLAGS.EDGES); const pickable = !!(flags & ENTITY_FLAGS.PICKABLE); const culled = !!(flags & ENTITY_FLAGS.CULLED); let colorFlag; if (!visible || culled || xrayed || (highlighted && !this.model.scene.highlightMaterial.glowThrough) || (selected && !this.model.scene.selectedMaterial.glowThrough)) { colorFlag = RENDER_PASSES.NOT_RENDERED; } else { if (meshTransparent) { colorFlag = RENDER_PASSES.COLOR_TRANSPARENT; } else { colorFlag = RENDER_PASSES.COLOR_OPAQUE; } } let silhouetteFlag; if (!visible || culled) { silhouetteFlag = RENDER_PASSES.NOT_RENDERED; } else if (selected) { silhouetteFlag = RENDER_PASSES.SILHOUETTE_SELECTED; } else if (highlighted) { silhouetteFlag = RENDER_PASSES.SILHOUETTE_HIGHLIGHTED; } else if (xrayed) { silhouetteFlag = RENDER_PASSES.SILHOUETTE_XRAYED; } else { silhouetteFlag = RENDER_PASSES.NOT_RENDERED; } let edgeFlag = 0; if (!visible || culled) { edgeFlag = RENDER_PASSES.NOT_RENDERED; } else if (selected) { edgeFlag = RENDER_PASSES.EDGES_SELECTED; } else if (highlighted) { edgeFlag = RENDER_PASSES.EDGES_HIGHLIGHTED; } else if (xrayed) { edgeFlag = RENDER_PASSES.EDGES_XRAYED; } else if (edges) { if (meshTransparent) { edgeFlag = RENDER_PASSES.EDGES_COLOR_TRANSPARENT; } else { edgeFlag = RENDER_PASSES.EDGES_COLOR_OPAQUE; } } else { edgeFlag = RENDER_PASSES.NOT_RENDERED; } let pickFlag = (visible && !culled && pickable) ? RENDER_PASSES.PICK : RENDER_PASSES.NOT_RENDERED; const clippableFlag = !!(flags & ENTITY_FLAGS.CLIPPABLE) ? 1 : 0; let vertFlag = 0; vertFlag |= colorFlag; vertFlag |= silhouetteFlag << 4; vertFlag |= edgeFlag << 8; vertFlag |= pickFlag << 12; vertFlag |= clippableFlag << 16; tempFloat32$1[0] = vertFlag; this._state.flagsBuf.setData(tempFloat32$1, portionId); } setOffset(portionId, offset) { if (!this._finalized) { throw "Not finalized"; } if (!this.model.scene.entityOffsetsEnabled) { this.model.error("Entity#offset not enabled for this Viewer"); // See Viewer entityOffsetsEnabled return; } tempVec3fa$1[0] = offset[0]; tempVec3fa$1[1] = offset[1]; tempVec3fa$1[2] = offset[2]; this._state.offsetsBuf.setData(tempVec3fa$1, portionId * 3, 3); } setMatrix(portionId, matrix) { //////////////////////////////////////// // TODO: Update portion matrix //////////////////////////////////////// if (!this._finalized) { throw "Not finalized"; } const offset = portionId * 4; tempFloat32Vec4$1[0] = matrix[0]; tempFloat32Vec4$1[1] = matrix[4]; tempFloat32Vec4$1[2] = matrix[8]; tempFloat32Vec4$1[3] = matrix[12]; this._state.modelMatrixCol0Buf.setData(tempFloat32Vec4$1, offset); tempFloat32Vec4$1[0] = matrix[1]; tempFloat32Vec4$1[1] = matrix[5]; tempFloat32Vec4$1[2] = matrix[9]; tempFloat32Vec4$1[3] = matrix[13]; this._state.modelMatrixCol1Buf.setData(tempFloat32Vec4$1, offset); tempFloat32Vec4$1[0] = matrix[2]; tempFloat32Vec4$1[1] = matrix[6]; tempFloat32Vec4$1[2] = matrix[10]; tempFloat32Vec4$1[3] = matrix[14]; this._state.modelMatrixCol2Buf.setData(tempFloat32Vec4$1, offset); } // ---------------------- NORMAL RENDERING ----------------------------------- drawColorOpaque(renderFlags, frameCtx) { if (this._numCulledLayerPortions === this._numPortions || this._numVisibleLayerPortions === 0 || this._numTransparentLayerPortions === this._numPortions || this._numXRayedLayerPortions === this._numPortions) { return; } if (this._renderers.colorRenderer) { this._renderers.colorRenderer.drawLayer(frameCtx, this, RENDER_PASSES.COLOR_OPAQUE); } } drawColorTransparent(renderFlags, frameCtx) { if (this._numCulledLayerPortions === this._numPortions || this._numVisibleLayerPortions === 0 || this._numTransparentLayerPortions === 0 || this._numXRayedLayerPortions === this._numPortions) { return; } if (this._renderers.colorRenderer) { this._renderers.colorRenderer.drawLayer(frameCtx, this, RENDER_PASSES.COLOR_TRANSPARENT); } } // ---------------------- RENDERING SAO POST EFFECT TARGETS -------------- drawDepth(renderFlags, frameCtx) { } drawNormals(renderFlags, frameCtx) { } // ---------------------- EMPHASIS RENDERING ----------------------------------- drawSilhouetteXRayed(renderFlags, frameCtx) { if (this._numCulledLayerPortions === this._numPortions || this._numVisibleLayerPortions === 0 || this._numXRayedLayerPortions === 0) { return; } if (this._renderers.silhouetteRenderer) { this._renderers.silhouetteRenderer.drawLayer(frameCtx, this, RENDER_PASSES.SILHOUETTE_XRAYED); } } drawSilhouetteHighlighted(renderFlags, frameCtx) { if (this._numCulledLayerPortions === this._numPortions || this._numVisibleLayerPortions === 0 || this._numHighlightedLayerPortions === 0) { return; } if (this._renderers.silhouetteRenderer) { this._renderers.silhouetteRenderer.drawLayer(frameCtx, this, RENDER_PASSES.SILHOUETTE_HIGHLIGHTED); } } drawSilhouetteSelected(renderFlags, frameCtx) { if (this._numCulledLayerPortions === this._numPortions || this._numVisibleLayerPortions === 0 || this._numSelectedLayerPortions === 0) { return; } if (this._renderers.silhouetteRenderer) { this._renderers.silhouetteRenderer.drawLayer(frameCtx, this, RENDER_PASSES.SILHOUETTE_SELECTED); } } // ---------------------- EDGES RENDERING ----------------------------------- drawEdgesColorOpaque(renderFlags, frameCtx) { } drawEdgesColorTransparent(renderFlags, frameCtx) { } drawEdgesXRayed(renderFlags, frameCtx) { } drawEdgesHighlighted(renderFlags, frameCtx) { } drawEdgesSelected(renderFlags, frameCtx) { } drawSnapInit(renderFlags, frameCtx) { if (this._numCulledLayerPortions === this._numPortions || this._numVisibleLayerPortions === 0) { return; } if (this._renderers.snapInitRenderer) { this._renderers.snapInitRenderer.drawLayer(frameCtx, this, RENDER_PASSES.PICK); } } drawSnap(renderFlags, frameCtx) { if (this._numCulledLayerPortions === this._numPortions || this._numVisibleLayerPortions === 0) { return; } if (this._renderers.snapRenderer) { this._renderers.snapRenderer.drawLayer(frameCtx, this, RENDER_PASSES.PICK); } } // ---------------------- OCCLUSION CULL RENDERING ----------------------------------- drawOcclusion(renderFlags, frameCtx) { } // ---------------------- SHADOW BUFFER RENDERING ----------------------------------- drawShadow(renderFlags, frameCtx) { } //---- PICKING ---------------------------------------------------------------------------------------------------- drawPickMesh(renderFlags, frameCtx) { } drawPickDepths(renderFlags, frameCtx) { } drawPickNormals(renderFlags, frameCtx) { } destroy() { const state = this._state; if (state.positionsBuf) { state.positionsBuf.destroy(); state.positionsBuf = null; } if (state.colorsBuf) { state.colorsBuf.destroy(); state.colorsBuf = null; } if (state.flagsBuf) { state.flagsBuf.destroy(); state.flagsBuf = null; } if (state.offsetsBuf) { state.offsetsBuf.destroy(); state.offsetsBuf = null; } if (state.modelMatrixCol0Buf) { state.modelMatrixCol0Buf.destroy(); state.modelMatrixCol0Buf = null; } if (state.modelMatrixCol1Buf) { state.modelMatrixCol1Buf.destroy(); state.modelMatrixCol1Buf = null; } if (state.modelMatrixCol2Buf) { state.modelMatrixCol2Buf.destroy(); state.modelMatrixCol2Buf = null; } state.destroy(); } } /** * @private */ class VBOBatchingPointsRenderer extends VBORenderer { _draw(drawCfg) { const {gl} = this._scene.canvas; const { state, frameCtx, incrementDrawState, } = drawCfg; gl.drawArrays(gl.POINTS, 0, state.positionsBuf.numItems); if (incrementDrawState) { frameCtx.drawArrays++; } } } /** * @private */ class VBOBatchingPointsColorRenderer extends VBOBatchingPointsRenderer { _getHash() { return this._scene._sectionPlanesState.getHash() + this._scene.pointsMaterial.hash; } drawLayer(frameCtx, layer, renderPass) { super.drawLayer(frameCtx, layer, renderPass, { incrementDrawState: true }); } _buildVertexShader() { const scene = this._scene; const sectionPlanesState = scene._sectionPlanesState; const clipping = sectionPlanesState.getNumAllocatedSectionPlanes() > 0; const pointsMaterial = scene.pointsMaterial; const src = []; src.push('#version 300 es'); src.push("// Points batching color vertex shader"); src.push("uniform int renderPass;"); src.push("in vec3 position;"); src.push("in vec4 color;"); src.push("in float flags;"); if (scene.entityOffsetsEnabled) { src.push("in vec3 offset;"); } this._addMatricesUniformBlockLines(src); src.push("uniform float pointSize;"); if (pointsMaterial.perspectivePoints) { src.push("uniform float nearPlaneHeight;"); } if (pointsMaterial.filterIntensity) { src.push("uniform vec2 intensityRange;"); } if (scene.logarithmicDepthBufferEnabled) { src.push("uniform float logDepthBufFC;"); src.push("out float vFragDepth;"); } if (clipping) { src.push("out vec4 vWorldPosition;"); src.push("out float vFlags;"); } src.push("out vec4 vColor;"); src.push("void main(void) {"); // colorFlag = NOT_RENDERED | COLOR_OPAQUE | COLOR_TRANSPARENT // renderPass = COLOR_OPAQUE src.push(`int colorFlag = int(flags) & 0xF;`); src.push(`if (colorFlag != renderPass) {`); src.push(" gl_Position = vec4(0.0, 0.0, 0.0, 0.0);"); // Cull vertex src.push("} else {"); if (pointsMaterial.filterIntensity) { src.push("float intensity = float(color.a) / 255.0;"); src.push("if (intensity < intensityRange[0] || intensity > intensityRange[1]) {"); src.push(" gl_Position = vec4(0.0, 0.0, 0.0, 0.0);"); // Cull vertex src.push("} else {"); } src.push("vec4 worldPosition = worldMatrix * (positionsDecodeMatrix * vec4(position, 1.0)); "); if (scene.entityOffsetsEnabled) { src.push("worldPosition.xyz = worldPosition.xyz + offset;"); } src.push("vec4 viewPosition = viewMatrix * worldPosition; "); src.push("vColor = vec4(float(color.r) / 255.0, float(color.g) / 255.0, float(color.b) / 255.0, 1.0);"); if (clipping) { src.push("vWorldPosition = worldPosition;"); src.push("vFlags = flags;"); } src.push("vec4 clipPos = projMatrix * viewPosition;"); if (scene.logarithmicDepthBufferEnabled) { src.push("vFragDepth = 1.0 + clipPos.w;"); } src.push("gl_Position = clipPos;"); if (pointsMaterial.perspectivePoints) { src.push("gl_PointSize = (nearPlaneHeight * pointSize) / clipPos.w;"); src.push("gl_PointSize = max(gl_PointSize, " + Math.floor(pointsMaterial.minPerspectivePointSize) + ".0);"); src.push("gl_PointSize = min(gl_PointSize, " + Math.floor(pointsMaterial.maxPerspectivePointSize) + ".0);"); } else { src.push("gl_PointSize = pointSize;"); } src.push("}"); if (pointsMaterial.filterIntensity) { src.push("}"); } src.push("}"); return src; } _buildFragmentShader() { const scene = this._scene; const sectionPlanesState = scene._sectionPlanesState; const clipping = sectionPlanesState.getNumAllocatedSectionPlanes() > 0; const src = []; src.push('#version 300 es'); src.push("// Points batching color fragment shader"); src.push("#ifdef GL_FRAGMENT_PRECISION_HIGH"); src.push("precision highp float;"); src.push("precision highp int;"); src.push("#else"); src.push("precision mediump float;"); src.push("precision mediump int;"); src.push("#endif"); if (scene.logarithmicDepthBufferEnabled) { src.push("uniform float logDepthBufFC;"); src.push("in float vFragDepth;"); } if (clipping) { src.push("in vec4 vWorldPosition;"); src.push("in float vFlags;"); for (let i = 0, len = sectionPlanesState.getNumAllocatedSectionPlanes(); i < len; i++) { src.push("uniform bool sectionPlaneActive" + i + ";"); src.push("uniform vec3 sectionPlanePos" + i + ";"); src.push("uniform vec3 sectionPlaneDir" + i + ";"); } } src.push("in vec4 vColor;"); src.push("out vec4 outColor;"); src.push("void main(void) {"); if (scene.pointsMaterial.roundPoints) { src.push(" vec2 cxy = 2.0 * gl_PointCoord - 1.0;"); src.push(" float r = dot(cxy, cxy);"); src.push(" if (r > 1.0) {"); src.push(" discard;"); src.push(" }"); } if (clipping) { src.push(" bool clippable = (int(vFlags) >> 16 & 0xF) == 1;"); src.push(" if (clippable) {"); src.push(" float dist = 0.0;"); for (let i = 0, len = sectionPlanesState.getNumAllocatedSectionPlanes(); i < len; i++) { src.push("if (sectionPlaneActive" + i + ") {"); src.push(" dist += clamp(dot(-sectionPlaneDir" + i + ".xyz, vWorldPosition.xyz - sectionPlanePos" + i + ".xyz), 0.0, 1000.0);"); src.push("}"); } src.push(" if (dist > 0.0) { discard; }"); src.push("}"); } src.push(" outColor = vColor;"); if (scene.logarithmicDepthBufferEnabled) { src.push("gl_FragDepth = log2( vFragDepth ) * logDepthBufFC * 0.5;"); } src.push("}"); return src; } } /** * @private */ class VBOBatchingPointsSilhouetteRenderer extends VBOBatchingPointsRenderer { _getHash() { return this._scene._sectionPlanesState.getHash() + this._scene.pointsMaterial.hash; } drawLayer(frameCtx, pointsBatchingLayer, renderPass) { super.drawLayer(frameCtx, pointsBatchingLayer, renderPass, { colorUniform: true }); } _buildVertexShader() { const scene = this._scene; const sectionPlanesState = scene._sectionPlanesState; const clipping = sectionPlanesState.getNumAllocatedSectionPlanes() > 0; const pointsMaterial = scene.pointsMaterial._state; const src = []; src.push ('#version 300 es'); src.push("// Points batching silhouette vertex shader"); src.push("uniform int renderPass;"); src.push("in vec3 position;"); if (scene.entityOffsetsEnabled) { src.push("in vec3 offset;"); } src.push("in float flags;"); this._addMatricesUniformBlockLines(src); src.push("uniform vec4 color;"); src.push("uniform float pointSize;"); if (pointsMaterial.perspectivePoints) { src.push("uniform float nearPlaneHeight;"); } if (scene.logarithmicDepthBufferEnabled) { src.push("uniform float logDepthBufFC;"); src.push("out float vFragDepth;"); } if (clipping) { src.push("out vec4 vWorldPosition;"); src.push("out float vFlags;"); } src.push("void main(void) {"); // silhouetteFlag = NOT_RENDERED | SILHOUETTE_HIGHLIGHTED | SILHOUETTE_SELECTED | SILHOUETTE_XRAYED // renderPass = SILHOUETTE_HIGHLIGHTED | SILHOUETTE_SELECTED | | SILHOUETTE_XRAYED src.push(`int silhouetteFlag = int(flags) >> 4 & 0xF;`); src.push(`if (silhouetteFlag != renderPass) {`); src.push(" gl_Position = vec4(0.0, 0.0, 0.0, 0.0);"); // Cull vertex src.push("} else {"); src.push(" vec4 worldPosition = worldMatrix * (positionsDecodeMatrix * vec4(position, 1.0)); "); if (scene.entityOffsetsEnabled) { src.push(" worldPosition.xyz = worldPosition.xyz + offset;"); } src.push("vec4 viewPosition = viewMatrix * worldPosition; "); if (clipping) { src.push("vWorldPosition = worldPosition;"); src.push("vFlags = flags;"); } src.push("vec4 clipPos = projMatrix * viewPosition;"); if (scene.logarithmicDepthBufferEnabled) { src.push("vFragDepth = 1.0 + clipPos.w;"); } src.push("gl_Position = clipPos;"); if (pointsMaterial.perspectivePoints) { src.push("gl_PointSize = (nearPlaneHeight * pointSize) / clipPos.w;"); src.push("gl_PointSize = max(gl_PointSize, " + Math.floor(pointsMaterial.minPerspectivePointSize) + ".0);"); src.push("gl_PointSize = min(gl_PointSize, " + Math.floor(pointsMaterial.maxPerspectivePointSize) + ".0);"); } else { src.push("gl_PointSize = pointSize;"); } src.push("}"); src.push("}"); return src; } _buildFragmentShader() { const scene = this._scene; const sectionPlanesState = scene._sectionPlanesState; let i; let len; const clipping = sectionPlanesState.getNumAllocatedSectionPlanes() > 0; const src = []; src.push ('#version 300 es'); src.push("// Points batching silhouette vertex shader"); src.push("#ifdef GL_FRAGMENT_PRECISION_HIGH"); src.push("precision highp float;"); src.push("precision highp int;"); src.push("#else"); src.push("precision mediump float;"); src.push("precision mediump int;"); src.push("#endif"); if (scene.logarithmicDepthBufferEnabled) { src.push("uniform float logDepthBufFC;"); src.push("in float vFragDepth;"); } if (clipping) { src.push("in vec4 vWorldPosition;"); src.push("in float vFlags;"); for (i = 0, len = sectionPlanesState.getNumAllocatedSectionPlanes(); i < len; i++) { src.push("uniform bool sectionPlaneActive" + i + ";"); src.push("uniform vec3 sectionPlanePos" + i + ";"); src.push("uniform vec3 sectionPlaneDir" + i + ";"); } } src.push("uniform vec4 color;"); src.push("out vec4 outColor;"); src.push("void main(void) {"); if (scene.pointsMaterial.roundPoints) { src.push(" vec2 cxy = 2.0 * gl_PointCoord - 1.0;"); src.push(" float r = dot(cxy, cxy);"); src.push(" if (r > 1.0) {"); src.push(" discard;"); src.push(" }"); } if (clipping) { src.push(" bool clippable = (int(vFlags) >> 16 & 0xF) == 1;"); src.push(" if (clippable) {"); src.push(" float dist = 0.0;"); for (i = 0, len = sectionPlanesState.getNumAllocatedSectionPlanes(); i < len; i++) { src.push("if (sectionPlaneActive" + i + ") {"); src.push(" dist += clamp(dot(-sectionPlaneDir" + i + ".xyz, vWorldPosition.xyz - sectionPlanePos" + i + ".xyz), 0.0, 1000.0);"); src.push("}"); } src.push(" if (dist > 0.0) { discard; }"); src.push("}"); } if (scene.logarithmicDepthBufferEnabled) { src.push("gl_FragDepth = log2( vFragDepth ) * logDepthBufFC * 0.5;"); } src.push("outColor = color;"); src.push("}"); return src; } } /** * @private */ class VBOBatchingPointsPickMeshRenderer extends VBOBatchingPointsRenderer { _getHash() { return this._scene._sectionPlanesState.getHash() + (this._scene.pointsMaterial.hash); } _buildVertexShader() { const scene = this._scene; const clipping = scene._sectionPlanesState.getNumAllocatedSectionPlanes() > 0; const pointsMaterial = scene.pointsMaterial._state; const src = []; src.push ('#version 300 es'); src.push("// Points batching pick mesh vertex shader"); src.push("uniform int renderPass;"); src.push("in vec3 position;"); if (scene.entityOffsetsEnabled) { src.push("in vec3 offset;"); } src.push("in float flags;"); src.push("in vec4 pickColor;"); src.push("uniform bool pickInvisible;"); this._addMatricesUniformBlockLines(src); this._addRemapClipPosLines(src); src.push("uniform float pointSize;"); if (pointsMaterial.perspectivePoints) { src.push("uniform float nearPlaneHeight;"); } if (scene.logarithmicDepthBufferEnabled) { src.push("uniform float logDepthBufFC;"); src.push("out float vFragDepth;"); } if (clipping) { src.push("out vec4 vWorldPosition;"); src.push("out float vFlags;"); } src.push("out vec4 vPickColor;"); src.push("void main(void) {"); // pickFlag = NOT_RENDERED | PICK // renderPass = PICK src.push(`int pickFlag = int(flags) >> 12 & 0xF;`); src.push(`if (pickFlag != renderPass) {`); src.push(" gl_Position = vec4(0.0, 0.0, 0.0, 0.0);"); // Cull vertex src.push(" } else {"); src.push(" vec4 worldPosition = worldMatrix * (positionsDecodeMatrix * vec4(position, 1.0)); "); if (scene.entityOffsetsEnabled) { src.push(" worldPosition.xyz = worldPosition.xyz + offset;"); } src.push(" vec4 viewPosition = viewMatrix * worldPosition; "); src.push(" vPickColor = vec4(float(pickColor.r) / 255.0, float(pickColor.g) / 255.0, float(pickColor.b) / 255.0, float(pickColor.a) / 255.0);"); if (clipping) { src.push(" vWorldPosition = worldPosition;"); src.push(" vFlags = flags;"); } src.push("vec4 clipPos = projMatrix * viewPosition;"); if (scene.logarithmicDepthBufferEnabled) { src.push("vFragDepth = 1.0 + clipPos.w;"); } src.push("gl_Position = remapClipPos(clipPos);"); if (pointsMaterial.perspectivePoints) { src.push("gl_PointSize = (nearPlaneHeight * pointSize) / clipPos.w;"); src.push("gl_PointSize = max(gl_PointSize, " + Math.floor(pointsMaterial.minPerspectivePointSize) + ".0);"); src.push("gl_PointSize = min(gl_PointSize, " + Math.floor(pointsMaterial.maxPerspectivePointSize) + ".0);"); } else { src.push("gl_PointSize = pointSize;"); } src.push("gl_PointSize += 10.0;"); src.push(" }"); src.push("}"); return src; } _buildFragmentShader() { const scene = this._scene; const sectionPlanesState = scene._sectionPlanesState; const clipping = sectionPlanesState.getNumAllocatedSectionPlanes() > 0; const src = []; src.push ('#version 300 es'); src.push("// Points batching pick mesh vertex shader"); src.push("#ifdef GL_FRAGMENT_PRECISION_HIGH"); src.push("precision highp float;"); src.push("precision highp int;"); src.push("#else"); src.push("precision mediump float;"); src.push("precision mediump int;"); src.push("#endif"); if (scene.logarithmicDepthBufferEnabled) { src.push("uniform float logDepthBufFC;"); src.push("in float vFragDepth;"); } if (clipping) { src.push("in vec4 vWorldPosition;"); src.push("in float vFlags;"); for (var i = 0; i < sectionPlanesState.getNumAllocatedSectionPlanes(); i++) { src.push("uniform bool sectionPlaneActive" + i + ";"); src.push("uniform vec3 sectionPlanePos" + i + ";"); src.push("uniform vec3 sectionPlaneDir" + i + ";"); } } src.push("in vec4 vPickColor;"); src.push("out vec4 outColor;"); src.push("void main(void) {"); if (scene.pointsMaterial.roundPoints) { src.push(" vec2 cxy = 2.0 * gl_PointCoord - 1.0;"); src.push(" float r = dot(cxy, cxy);"); src.push(" if (r > 1.0) {"); src.push(" discard;"); src.push(" }"); } if (clipping) { src.push(" bool clippable = (int(vFlags) >> 16 & 0xF) == 1;"); src.push(" if (clippable) {"); src.push(" float dist = 0.0;"); for (let i = 0; i < sectionPlanesState.getNumAllocatedSectionPlanes(); i++) { src.push(" if (sectionPlaneActive" + i + ") {"); src.push(" dist += clamp(dot(-sectionPlaneDir" + i + ".xyz, vWorldPosition.xyz - sectionPlanePos" + i + ".xyz), 0.0, 1000.0);"); src.push(" }"); } src.push(" if (dist > 0.0) { discard; }"); src.push(" }"); } if (scene.logarithmicDepthBufferEnabled) { src.push("gl_FragDepth = log2( vFragDepth ) * logDepthBufFC * 0.5;"); } src.push(" outColor = vPickColor; "); src.push("}"); return src; } } /** * @private */ class VBOBatchingPointsPickDepthRenderer extends VBOBatchingPointsRenderer { _getHash() { return this._scene._sectionPlanesState.getHash() + (this._scene.pointsMaterial.hash); } _buildVertexShader() { const scene = this._scene; const clipping = scene._sectionPlanesState.getNumAllocatedSectionPlanes() > 0; const pointsMaterial = scene.pointsMaterial._state; const src = []; src.push('#version 300 es'); src.push("// Points batched pick depth vertex shader"); src.push("uniform int renderPass;"); src.push("in vec3 position;"); if (scene.entityOffsetsEnabled) { src.push("in vec3 offset;"); } src.push("in float flags;"); src.push("uniform bool pickInvisible;"); this._addMatricesUniformBlockLines(src); this._addRemapClipPosLines(src); src.push("uniform float pointSize;"); if (pointsMaterial.perspectivePoints) { src.push("uniform float nearPlaneHeight;"); } if (scene.logarithmicDepthBufferEnabled) { src.push("uniform float logDepthBufFC;"); src.push("out float vFragDepth;"); } if (clipping) { src.push("out vec4 vWorldPosition;"); src.push("out float vFlags;"); } src.push("out vec4 vViewPosition;"); src.push("void main(void) {"); // pickFlag = NOT_RENDERED | PICK // renderPass = PICK src.push(`int pickFlag = int(flags) >> 12 & 0xF;`); src.push(`if (pickFlag != renderPass) {`); src.push(" gl_Position = vec4(0.0, 0.0, 0.0, 0.0);"); // Cull vertex src.push(" } else {"); src.push(" vec4 worldPosition = worldMatrix * (positionsDecodeMatrix * vec4(position, 1.0)); "); if (scene.entityOffsetsEnabled) { src.push(" worldPosition.xyz = worldPosition.xyz + offset;"); } src.push(" vec4 viewPosition = viewMatrix * worldPosition; "); if (clipping) { src.push(" vWorldPosition = worldPosition;"); src.push(" vFlags = flags;"); } src.push("vViewPosition = viewPosition;"); src.push("vec4 clipPos = projMatrix * viewPosition;"); if (scene.logarithmicDepthBufferEnabled) { src.push("vFragDepth = 1.0 + clipPos.w;"); } src.push("gl_Position = remapClipPos(clipPos);"); if (pointsMaterial.perspectivePoints) { src.push("gl_PointSize = (nearPlaneHeight * pointSize) / clipPos.w;"); src.push("gl_PointSize = max(gl_PointSize, " + Math.floor(pointsMaterial.minPerspectivePointSize) + ".0);"); src.push("gl_PointSize = min(gl_PointSize, " + Math.floor(pointsMaterial.maxPerspectivePointSize) + ".0);"); } else { src.push("gl_PointSize = pointSize;"); } src.push("gl_PointSize += 10.0;"); src.push(" }"); src.push("}"); return src; } _buildFragmentShader() { const scene = this._scene; const sectionPlanesState = scene._sectionPlanesState; const clipping = sectionPlanesState.getNumAllocatedSectionPlanes() > 0; const src = []; src.push('#version 300 es'); src.push("// Points batched pick depth fragment shader"); src.push("#ifdef GL_FRAGMENT_PRECISION_HIGH"); src.push("precision highp float;"); src.push("precision highp int;"); src.push("#else"); src.push("precision mediump float;"); src.push("precision mediump int;"); src.push("#endif"); if (scene.logarithmicDepthBufferEnabled) { src.push("uniform float logDepthBufFC;"); src.push("in float vFragDepth;"); } src.push("uniform float pickZNear;"); src.push("uniform float pickZFar;"); if (clipping) { src.push("in vec4 vWorldPosition;"); src.push("in float vFlags;"); for (let i = 0; i < sectionPlanesState.getNumAllocatedSectionPlanes(); i++) { src.push("uniform bool sectionPlaneActive" + i + ";"); src.push("uniform vec3 sectionPlanePos" + i + ";"); src.push("uniform vec3 sectionPlaneDir" + i + ";"); } } src.push("in vec4 vViewPosition;"); src.push("vec4 packDepth(const in float depth) {"); src.push(" const vec4 bitShift = vec4(256.0*256.0*256.0, 256.0*256.0, 256.0, 1.0);"); src.push(" const vec4 bitMask = vec4(0.0, 1.0/256.0, 1.0/256.0, 1.0/256.0);"); src.push(" vec4 res = fract(depth * bitShift);"); src.push(" res -= res.xxyz * bitMask;"); src.push(" return res;"); src.push("}"); src.push("out vec4 outColor;"); src.push("void main(void) {"); if (scene.pointsMaterial.roundPoints) { src.push(" vec2 cxy = 2.0 * gl_PointCoord - 1.0;"); src.push(" float r = dot(cxy, cxy);"); src.push(" if (r > 1.0) {"); src.push(" discard;"); src.push(" }"); } if (clipping) { src.push(" bool clippable = (int(vFlags) >> 16 & 0xF) == 1;"); src.push(" if (clippable) {"); src.push(" float dist = 0.0;"); for (var i = 0; i < sectionPlanesState.getNumAllocatedSectionPlanes(); i++) { src.push(" if (sectionPlaneActive" + i + ") {"); src.push(" dist += clamp(dot(-sectionPlaneDir" + i + ".xyz, vWorldPosition.xyz - sectionPlanePos" + i + ".xyz), 0.0, 1000.0);"); src.push(" }"); } src.push(" if (dist > 0.0) { discard; }"); src.push(" }"); } if (scene.logarithmicDepthBufferEnabled) { src.push("gl_FragDepth = log2( vFragDepth ) * logDepthBufFC * 0.5;"); } src.push(" float zNormalizedDepth = abs((pickZNear + vViewPosition.z) / (pickZFar - pickZNear));"); src.push(" outColor = packDepth(zNormalizedDepth); "); // Must be linear depth src.push("}"); return src; } } /** * @private */ class VBOBatchingPointsOcclusionRenderer extends VBOBatchingPointsRenderer { _getHash() { return this._scene._sectionPlanesState.getHash() + (this._scene.pointsMaterial.hash); } _buildVertexShader() { const scene = this._scene; const clipping = scene._sectionPlanesState.getNumAllocatedSectionPlanes() > 0; const pointsMaterial = scene.pointsMaterial._state; const src = []; src.push('#version 300 es'); src.push("// Points batching occlusion vertex shader"); src.push("uniform int renderPass;"); src.push("in vec3 position;"); if (scene.entityOffsetsEnabled) { src.push("in vec3 offset;"); } src.push("in float flags;"); this._addMatricesUniformBlockLines(src); src.push("uniform float pointSize;"); if (pointsMaterial.perspectivePoints) { src.push("uniform float nearPlaneHeight;"); } if (scene.logarithmicDepthBufferEnabled) { src.push("uniform float logDepthBufFC;"); src.push("out float vFragDepth;"); } if (clipping) { src.push("out vec4 vWorldPosition;"); src.push("out float vFlags;"); } src.push("void main(void) {"); // colorFlag = NOT_RENDERED | COLOR_OPAQUE | COLOR_TRANSPARENT // renderPass = COLOR_OPAQUE // Only opaque objects can be occluders src.push(`int colorFlag = int(flags) & 0xF;`); src.push(`if (colorFlag != renderPass) {`); src.push(" gl_Position = vec4(0.0, 0.0, 0.0, 0.0);"); // Cull vertex src.push(" } else {"); src.push(" vec4 worldPosition = worldMatrix * (positionsDecodeMatrix * vec4(position, 1.0)); "); if (scene.entityOffsetsEnabled) { src.push(" worldPosition.xyz = worldPosition.xyz + offset;"); } src.push(" vec4 viewPosition = viewMatrix * worldPosition; "); if (clipping) { src.push(" vWorldPosition = worldPosition;"); src.push(" vFlags = flags;"); } src.push("vec4 clipPos = projMatrix * viewPosition;"); if (scene.logarithmicDepthBufferEnabled) { src.push("vFragDepth = 1.0 + clipPos.w;"); } src.push(" gl_Position = clipPos;"); if (pointsMaterial.perspectivePoints) { src.push("gl_PointSize = (nearPlaneHeight * pointSize) / clipPos.w;"); src.push("gl_PointSize = max(gl_PointSize, " + Math.floor(pointsMaterial.minPerspectivePointSize) + ".0);"); src.push("gl_PointSize = min(gl_PointSize, " + Math.floor(pointsMaterial.maxPerspectivePointSize) + ".0);"); } else { src.push("gl_PointSize = pointSize;"); } src.push(" }"); src.push("}"); return src; } _buildFragmentShader() { const scene = this._scene; const sectionPlanesState = scene._sectionPlanesState; const clipping = sectionPlanesState.getNumAllocatedSectionPlanes() > 0; const src = []; src.push('#version 300 es'); src.push("// Points batching occlusion fragment shader"); src.push("#ifdef GL_FRAGMENT_PRECISION_HIGH"); src.push("precision highp float;"); src.push("precision highp int;"); src.push("#else"); src.push("precision mediump float;"); src.push("precision mediump int;"); src.push("#endif"); if (scene.logarithmicDepthBufferEnabled) { src.push("uniform float logDepthBufFC;"); src.push("in float vFragDepth;"); } if (clipping) { src.push("in vec4 vWorldPosition;"); src.push("in float vFlags;"); for (let i = 0; i < sectionPlanesState.getNumAllocatedSectionPlanes(); i++) { src.push("uniform bool sectionPlaneActive" + i + ";"); src.push("uniform vec3 sectionPlanePos" + i + ";"); src.push("uniform vec3 sectionPlaneDir" + i + ";"); } } src.push("out vec4 outColor;"); src.push("void main(void) {"); if (scene.pointsMaterial.roundPoints) { src.push(" vec2 cxy = 2.0 * gl_PointCoord - 1.0;"); src.push(" float r = dot(cxy, cxy);"); src.push(" if (r > 1.0) {"); src.push(" discard;"); src.push(" }"); } if (clipping) { src.push(" bool clippable = (int(vFlags) >> 16 & 0xF) == 1;"); src.push(" if (clippable) {"); src.push(" float dist = 0.0;"); for (let i = 0; i < sectionPlanesState.getNumAllocatedSectionPlanes(); i++) { src.push(" if (sectionPlaneActive" + i + ") {"); src.push(" dist += clamp(dot(-sectionPlaneDir" + i + ".xyz, vWorldPosition.xyz - sectionPlanePos" + i + ".xyz), 0.0, 1000.0);"); src.push(" }"); } src.push(" if (dist > 0.0) { discard; }"); src.push(" }"); } if (scene.logarithmicDepthBufferEnabled) { src.push("gl_FragDepth = log2( vFragDepth ) * logDepthBufFC * 0.5;"); } src.push(" outColor = vec4(0.0, 0.0, 1.0, 1.0); "); // Occluders are blue src.push("}"); return src; } } const tempVec3a$u = math.vec3(); const tempVec3b$q = math.vec3(); const tempVec3c$l = math.vec3(); const tempVec3d$6 = math.vec3(); const tempMat4a$i = math.mat4(); /** * @private */ class VBOBatchingPointsSnapInitRenderer extends VBORenderer { drawLayer(frameCtx, batchingLayer, renderPass) { if (!this._program) { this._allocate(); if (this.errors) { return; } } if (frameCtx.lastProgramId !== this._program.id) { frameCtx.lastProgramId = this._program.id; this._bindProgram(); } const model = batchingLayer.model; const scene = model.scene; const camera = scene.camera; const gl = scene.canvas.gl; const state = batchingLayer._state; const origin = batchingLayer._state.origin; const {position, rotationMatrix} = model; const aabb = batchingLayer.aabb; // Per-layer AABB for best RTC accuracy const viewMatrix = frameCtx.pickViewMatrix || camera.viewMatrix; if (this._vaoCache.has(batchingLayer)) { gl.bindVertexArray(this._vaoCache.get(batchingLayer)); } else { this._vaoCache.set(batchingLayer, this._makeVAO(state)); } const coordinateScaler = tempVec3a$u; coordinateScaler[0] = math.safeInv(aabb[3] - aabb[0]) * math.MAX_INT; coordinateScaler[1] = math.safeInv(aabb[4] - aabb[1]) * math.MAX_INT; coordinateScaler[2] = math.safeInv(aabb[5] - aabb[2]) * math.MAX_INT; frameCtx.snapPickCoordinateScale[0] = math.safeInv(coordinateScaler[0]); frameCtx.snapPickCoordinateScale[1] = math.safeInv(coordinateScaler[1]); frameCtx.snapPickCoordinateScale[2] = math.safeInv(coordinateScaler[2]); let rtcViewMatrix; let rtcCameraEye; if (origin || position[0] !== 0 || position[1] !== 0 || position[2] !== 0) { const rtcOrigin = tempVec3b$q; if (origin) { const rotatedOrigin = tempVec3c$l; math.transformPoint3(rotationMatrix, origin, rotatedOrigin); rtcOrigin[0] = rotatedOrigin[0]; rtcOrigin[1] = rotatedOrigin[1]; rtcOrigin[2] = rotatedOrigin[2]; } else { rtcOrigin[0] = 0; rtcOrigin[1] = 0; rtcOrigin[2] = 0; } rtcOrigin[0] += position[0]; rtcOrigin[1] += position[1]; rtcOrigin[2] += position[2]; rtcViewMatrix = createRTCViewMat(viewMatrix, rtcOrigin, tempMat4a$i); rtcCameraEye = tempVec3d$6; rtcCameraEye[0] = camera.eye[0] - rtcOrigin[0]; rtcCameraEye[1] = camera.eye[1] - rtcOrigin[1]; rtcCameraEye[2] = camera.eye[2] - rtcOrigin[2]; frameCtx.snapPickOrigin[0] = rtcOrigin[0]; frameCtx.snapPickOrigin[1] = rtcOrigin[1]; frameCtx.snapPickOrigin[2] = rtcOrigin[2]; } else { rtcViewMatrix = viewMatrix; rtcCameraEye = camera.eye; frameCtx.snapPickOrigin[0] = 0; frameCtx.snapPickOrigin[1] = 0; frameCtx.snapPickOrigin[2] = 0; } gl.uniform3fv(this._uCameraEyeRtc, rtcCameraEye); gl.uniform2fv(this.uVectorA, frameCtx.snapVectorA); gl.uniform2fv(this.uInverseVectorAB, frameCtx.snapInvVectorAB); gl.uniform1i(this._uLayerNumber, frameCtx.snapPickLayerNumber); gl.uniform3fv(this._uCoordinateScaler, coordinateScaler); gl.uniform1i(this._uRenderPass, renderPass); gl.uniform1i(this._uPickInvisible, frameCtx.pickInvisible); gl.uniform1f(this._uPointSize, 1.0); let offset = 0; const mat4Size = 4 * 4; this._matricesUniformBlockBufferData.set(rotationMatrix, 0); this._matricesUniformBlockBufferData.set(rtcViewMatrix, offset += mat4Size); this._matricesUniformBlockBufferData.set(camera.projMatrix, offset += mat4Size); this._matricesUniformBlockBufferData.set(state.positionsDecodeMatrix, offset += mat4Size); gl.bindBuffer(gl.UNIFORM_BUFFER, this._matricesUniformBlockBuffer); gl.bufferData(gl.UNIFORM_BUFFER, this._matricesUniformBlockBufferData, gl.DYNAMIC_DRAW); gl.bindBufferBase( gl.UNIFORM_BUFFER, this._matricesUniformBlockBufferBindingPoint, this._matricesUniformBlockBuffer); { const logDepthBufFC = 2.0 / (Math.log(frameCtx.pickZFar + 1.0) / Math.LN2); // TODO: Far from pick project matrix? gl.uniform1f(this._uLogDepthBufFC, logDepthBufFC); } this.setSectionPlanesStateUniforms(batchingLayer); //============================================================= // TODO: Use drawElements count and offset to draw only one entity //============================================================= gl.drawArrays(gl.POINTS, 0, state.positionsBuf.numItems); gl.bindVertexArray(null); } _allocate() { super._allocate(); const program = this._program; { this._uLogDepthBufFC = program.getLocation("logDepthBufFC"); } this._uCameraEyeRtc = program.getLocation("uCameraEyeRtc"); this.uVectorA = program.getLocation("snapVectorA"); this.uInverseVectorAB = program.getLocation("snapInvVectorAB"); this._uLayerNumber = program.getLocation("layerNumber"); this._uCoordinateScaler = program.getLocation("coordinateScaler"); this._uPointSize = program.getLocation("pointSize"); } _bindProgram() { this._program.bind(); } _buildVertexShader() { const scene = this._scene; const clipping = scene._sectionPlanesState.getNumAllocatedSectionPlanes() > 0; const src = []; src.push ('#version 300 es'); src.push("// VBOBatchingPointsSnapInitRenderer vertex shader"); src.push("#ifdef GL_FRAGMENT_PRECISION_HIGH"); src.push("precision highp float;"); src.push("precision highp int;"); src.push("precision highp usampler2D;"); src.push("precision highp isampler2D;"); src.push("precision highp sampler2D;"); src.push("#else"); src.push("precision mediump float;"); src.push("precision mediump int;"); src.push("precision mediump usampler2D;"); src.push("precision mediump isampler2D;"); src.push("precision mediump sampler2D;"); src.push("#endif"); src.push("uniform int renderPass;"); src.push("in vec4 pickColor;"); src.push("in vec3 position;"); if (scene.entityOffsetsEnabled) { src.push("in vec3 offset;"); } src.push("in float flags;"); src.push("uniform bool pickInvisible;"); this._addMatricesUniformBlockLines(src); src.push("uniform vec3 uCameraEyeRtc;"); src.push("uniform vec2 snapVectorA;"); src.push("uniform vec2 snapInvVectorAB;"); src.push("uniform float pointSize;"); { src.push("uniform float logDepthBufFC;"); src.push("out float vFragDepth;"); src.push("out float isPerspective;"); } src.push("bool isPerspectiveMatrix(mat4 m) {"); src.push(" return (m[2][3] == - 1.0);"); src.push("}"); src.push("vec2 remapClipPos(vec2 clipPos) {"); src.push(" float x = (clipPos.x - snapVectorA.x) * snapInvVectorAB.x;"); src.push(" float y = (clipPos.y - snapVectorA.y) * snapInvVectorAB.y;"); src.push(" return vec2(x, y);"); src.push("}"); src.push("flat out vec4 vPickColor;"); src.push("out vec4 vWorldPosition;"); if (clipping) { src.push("out float vFlags;"); } src.push("out highp vec3 relativeToOriginPosition;"); src.push("void main(void) {"); // pickFlag = NOT_RENDERED | PICK // renderPass = PICK src.push(`int pickFlag = int(flags) >> 12 & 0xF;`); src.push(`if (pickFlag != renderPass) {`); src.push(" gl_Position = vec4(0.0, 0.0, 0.0, 0.0);"); // Cull vertex src.push(" } else {"); src.push(" vec4 worldPosition = worldMatrix * (positionsDecodeMatrix * vec4(position, 1.0)); "); if (scene.entityOffsetsEnabled) { src.push(" worldPosition.xyz = worldPosition.xyz + offset;"); } src.push(" relativeToOriginPosition = worldPosition.xyz;"); src.push(" vec4 viewPosition = viewMatrix * worldPosition; "); src.push(" vWorldPosition = worldPosition;"); if (clipping) { src.push(" vFlags = flags;"); } src.push("vPickColor = pickColor;"); src.push("vec4 clipPos = projMatrix * viewPosition;"); src.push("float tmp = clipPos.w;"); src.push("clipPos.xyzw /= tmp;"); src.push("clipPos.xy = remapClipPos(clipPos.xy);"); src.push("clipPos.xyzw *= tmp;"); { src.push("vFragDepth = 1.0 + clipPos.w;"); src.push("isPerspective = float (isPerspectiveMatrix(projMatrix));"); } src.push("gl_Position = clipPos;"); src.push("gl_PointSize = pointSize;"); src.push(" }"); src.push("}"); return src; } _buildFragmentShader() { const scene = this._scene; const sectionPlanesState = scene._sectionPlanesState; const clipping = sectionPlanesState.getNumAllocatedSectionPlanes() > 0; const src = []; src.push ('#version 300 es'); src.push("// VBOBatchingPointsSnapInitRenderer fragment shader"); src.push("#ifdef GL_FRAGMENT_PRECISION_HIGH"); src.push("precision highp float;"); src.push("precision highp int;"); src.push("#else"); src.push("precision mediump float;"); src.push("precision mediump int;"); src.push("#endif"); { src.push("in float isPerspective;"); src.push("uniform float logDepthBufFC;"); src.push("in float vFragDepth;"); } src.push("uniform int layerNumber;"); src.push("uniform vec3 coordinateScaler;"); src.push("in vec4 vWorldPosition;"); src.push("flat in vec4 vPickColor;"); if (clipping) { src.push("in float vFlags;"); for (let i = 0; i < sectionPlanesState.getNumAllocatedSectionPlanes(); i++) { src.push("uniform bool sectionPlaneActive" + i + ";"); src.push("uniform vec3 sectionPlanePos" + i + ";"); src.push("uniform vec3 sectionPlaneDir" + i + ";"); } } src.push("in highp vec3 relativeToOriginPosition;"); src.push("layout(location = 0) out highp ivec4 outCoords;"); src.push("layout(location = 1) out highp ivec4 outNormal;"); src.push("layout(location = 2) out lowp uvec4 outPickColor;"); src.push("void main(void) {"); if (clipping) { src.push(" bool clippable = (int(vFlags) >> 16 & 0xF) == 1;"); src.push(" if (clippable) {"); src.push(" float dist = 0.0;"); for (var i = 0; i < sectionPlanesState.getNumAllocatedSectionPlanes(); i++) { src.push(" if (sectionPlaneActive" + i + ") {"); src.push(" dist += clamp(dot(-sectionPlaneDir" + i + ".xyz, vWorldPosition.xyz - sectionPlanePos" + i + ".xyz), 0.0, 1000.0);"); src.push(" }"); } src.push(" if (dist > 0.0) { discard; }"); src.push(" }"); } { src.push(" float dx = dFdx(vFragDepth);"); src.push(" float dy = dFdy(vFragDepth);"); src.push(" float diff = sqrt(dx*dx+dy*dy);"); src.push(" gl_FragDepth = isPerspective == 0.0 ? gl_FragCoord.z : log2( vFragDepth + diff ) * logDepthBufFC * 0.5;"); } src.push("outCoords = ivec4(relativeToOriginPosition.xyz*coordinateScaler.xyz, -layerNumber);"); // src.push("vec3 xTangent = dFdx( vWorldPosition.xyz );"); // src.push("vec3 yTangent = dFdy( vWorldPosition.xyz );"); // src.push("vec3 worldNormal = normalize( cross( xTangent, yTangent ) );"); src.push(`outNormal = ivec4(1.0, 1.0, 1.0, 1.0);`); src.push("outPickColor = uvec4(vPickColor);"); src.push("}"); return src; } webglContextRestored() { this._program = null; } destroy() { if (this._program) { this._program.destroy(); } this._program = null; } } const tempVec3a$t = math.vec3(); const tempVec3b$p = math.vec3(); const tempVec3c$k = math.vec3(); const tempVec3d$5 = math.vec3(); const tempMat4a$h = math.mat4(); /** * @private */ class VBOBatchingPointsSnapRenderer extends VBORenderer{ _getHash() { return this._scene._sectionPlanesState.getHash() + (this._scene.pointsMaterial.hash); } drawLayer(frameCtx, batchingLayer, renderPass) { if (!this._program) { this._allocate(); if (this.errors) { return; } } if (frameCtx.lastProgramId !== this._program.id) { frameCtx.lastProgramId = this._program.id; this._bindProgram(); } const model = batchingLayer.model; const scene = model.scene; const camera = scene.camera; const gl = scene.canvas.gl; const state = batchingLayer._state; const origin = batchingLayer._state.origin; const {position, rotationMatrix} = model; const aabb = batchingLayer.aabb; // Per-layer AABB for best RTC accuracy const viewMatrix = frameCtx.pickViewMatrix || camera.viewMatrix; if (this._vaoCache.has(batchingLayer)) { gl.bindVertexArray(this._vaoCache.get(batchingLayer)); } else { this._vaoCache.set(batchingLayer, this._makeVAO(state)); } const coordinateScaler = tempVec3a$t; coordinateScaler[0] = math.safeInv(aabb[3] - aabb[0]) * math.MAX_INT; coordinateScaler[1] = math.safeInv(aabb[4] - aabb[1]) * math.MAX_INT; coordinateScaler[2] = math.safeInv(aabb[5] - aabb[2]) * math.MAX_INT; frameCtx.snapPickCoordinateScale[0] = math.safeInv(coordinateScaler[0]); frameCtx.snapPickCoordinateScale[1] = math.safeInv(coordinateScaler[1]); frameCtx.snapPickCoordinateScale[2] = math.safeInv(coordinateScaler[2]); let rtcViewMatrix; let rtcCameraEye; if (origin || position[0] !== 0 || position[1] !== 0 || position[2] !== 0) { const rtcOrigin = tempVec3b$p; if (origin) { const rotatedOrigin = tempVec3c$k; math.transformPoint3(rotationMatrix, origin, rotatedOrigin); rtcOrigin[0] = rotatedOrigin[0]; rtcOrigin[1] = rotatedOrigin[1]; rtcOrigin[2] = rotatedOrigin[2]; } else { rtcOrigin[0] = 0; rtcOrigin[1] = 0; rtcOrigin[2] = 0; } rtcOrigin[0] += position[0]; rtcOrigin[1] += position[1]; rtcOrigin[2] += position[2]; rtcViewMatrix = createRTCViewMat(viewMatrix, rtcOrigin, tempMat4a$h); rtcCameraEye = tempVec3d$5; rtcCameraEye[0] = camera.eye[0] - rtcOrigin[0]; rtcCameraEye[1] = camera.eye[1] - rtcOrigin[1]; rtcCameraEye[2] = camera.eye[2] - rtcOrigin[2]; frameCtx.snapPickOrigin[0] = rtcOrigin[0]; frameCtx.snapPickOrigin[1] = rtcOrigin[1]; frameCtx.snapPickOrigin[2] = rtcOrigin[2]; } else { rtcViewMatrix = viewMatrix; rtcCameraEye = camera.eye; frameCtx.snapPickOrigin[0] = 0; frameCtx.snapPickOrigin[1] = 0; frameCtx.snapPickOrigin[2] = 0; } gl.uniform3fv(this._uCameraEyeRtc, rtcCameraEye); gl.uniform2fv(this.uVectorA, frameCtx.snapVectorA); gl.uniform2fv(this.uInverseVectorAB, frameCtx.snapInvVectorAB); gl.uniform1i(this._uLayerNumber, frameCtx.snapPickLayerNumber); gl.uniform3fv(this._uCoordinateScaler, coordinateScaler); gl.uniform1i(this._uRenderPass, renderPass); gl.uniform1i(this._uPickInvisible, frameCtx.pickInvisible); let offset = 0; const mat4Size = 4 * 4; this._matricesUniformBlockBufferData.set(rotationMatrix, 0); this._matricesUniformBlockBufferData.set(rtcViewMatrix, offset += mat4Size); this._matricesUniformBlockBufferData.set(camera.projMatrix, offset += mat4Size); this._matricesUniformBlockBufferData.set(state.positionsDecodeMatrix, offset += mat4Size); gl.bindBuffer(gl.UNIFORM_BUFFER, this._matricesUniformBlockBuffer); gl.bufferData(gl.UNIFORM_BUFFER, this._matricesUniformBlockBufferData, gl.DYNAMIC_DRAW); gl.bindBufferBase( gl.UNIFORM_BUFFER, this._matricesUniformBlockBufferBindingPoint, this._matricesUniformBlockBuffer); { const logDepthBufFC = 2.0 / (Math.log(frameCtx.pickZFar + 1.0) / Math.LN2); // TODO: Far from pick project matrix? gl.uniform1f(this._uLogDepthBufFC, logDepthBufFC); } this.setSectionPlanesStateUniforms(batchingLayer); //============================================================= // TODO: Use drawElements count and offset to draw only one entity //============================================================= gl.drawArrays(gl.POINTS, 0, state.positionsBuf.numItems); gl.bindVertexArray(null); } _allocate() { super._allocate(); const program = this._program; { this._uLogDepthBufFC = program.getLocation("logDepthBufFC"); } this._uCameraEyeRtc = program.getLocation("uCameraEyeRtc"); this.uVectorA = program.getLocation("snapVectorA"); this.uInverseVectorAB = program.getLocation("snapInvVectorAB"); this._uLayerNumber = program.getLocation("layerNumber"); this._uCoordinateScaler = program.getLocation("coordinateScaler"); } _bindProgram() { this._program.bind(); } _buildVertexShader() { const scene = this._scene; const clipping = scene._sectionPlanesState.getNumAllocatedSectionPlanes() > 0; scene.pointsMaterial._state; const src = []; src.push ('#version 300 es'); src.push("// VBOBatchingPointsSnapRenderer vertex shader"); src.push("#ifdef GL_FRAGMENT_PRECISION_HIGH"); src.push("precision highp float;"); src.push("precision highp int;"); src.push("precision highp usampler2D;"); src.push("precision highp isampler2D;"); src.push("precision highp sampler2D;"); src.push("#else"); src.push("precision mediump float;"); src.push("precision mediump int;"); src.push("precision mediump usampler2D;"); src.push("precision mediump isampler2D;"); src.push("precision mediump sampler2D;"); src.push("#endif"); src.push("uniform int renderPass;"); src.push("in vec3 position;"); if (scene.entityOffsetsEnabled) { src.push("in vec3 offset;"); } src.push("in float flags;"); src.push("uniform bool pickInvisible;"); this._addMatricesUniformBlockLines(src); src.push("uniform vec3 uCameraEyeRtc;"); src.push("uniform vec2 snapVectorA;"); src.push("uniform vec2 snapInvVectorAB;"); { src.push("uniform float logDepthBufFC;"); src.push("out float vFragDepth;"); src.push("bool isPerspectiveMatrix(mat4 m) {"); src.push(" return (m[2][3] == - 1.0);"); src.push("}"); src.push("out float isPerspective;"); } src.push("vec2 remapClipPos(vec2 clipPos) {"); src.push(" float x = (clipPos.x - snapVectorA.x) * snapInvVectorAB.x;"); src.push(" float y = (clipPos.y - snapVectorA.y) * snapInvVectorAB.y;"); src.push(" return vec2(x, y);"); src.push("}"); if (clipping) { src.push("out vec4 vWorldPosition;"); src.push("out float vFlags;"); } src.push("out highp vec3 relativeToOriginPosition;"); src.push("void main(void) {"); // pickFlag = NOT_RENDERED | PICK // renderPass = PICK src.push(`int pickFlag = int(flags) >> 12 & 0xF;`); src.push(`if (pickFlag != renderPass) {`); src.push(" gl_Position = vec4(0.0, 0.0, 0.0, 0.0);"); // Cull vertex src.push(" } else {"); src.push(" vec4 worldPosition = worldMatrix * (positionsDecodeMatrix * vec4(position, 1.0)); "); if (scene.entityOffsetsEnabled) { src.push(" worldPosition.xyz = worldPosition.xyz + offset;"); } src.push("relativeToOriginPosition = worldPosition.xyz;"); src.push(" vec4 viewPosition = viewMatrix * worldPosition; "); if (clipping) { src.push(" vWorldPosition = worldPosition;"); src.push(" vFlags = flags;"); } src.push("vec4 clipPos = projMatrix * viewPosition;"); src.push("float tmp = clipPos.w;"); src.push("clipPos.xyzw /= tmp;"); src.push("clipPos.xy = remapClipPos(clipPos.xy);"); src.push("clipPos.xyzw *= tmp;"); { src.push("vFragDepth = 1.0 + clipPos.w;"); src.push("isPerspective = float (isPerspectiveMatrix(projMatrix));"); } src.push("gl_Position = clipPos;"); src.push("gl_PointSize = 1.0;"); // Windows needs this? src.push(" }"); src.push("}"); return src; } _buildFragmentShader() { const scene = this._scene; const sectionPlanesState = scene._sectionPlanesState; const clipping = sectionPlanesState.getNumAllocatedSectionPlanes() > 0; const src = []; src.push ('#version 300 es'); src.push("// VBOBatchingPointsSnapRenderer fragment shader"); src.push("#ifdef GL_FRAGMENT_PRECISION_HIGH"); src.push("precision highp float;"); src.push("precision highp int;"); src.push("#else"); src.push("precision mediump float;"); src.push("precision mediump int;"); src.push("#endif"); { src.push("in float isPerspective;"); src.push("uniform float logDepthBufFC;"); src.push("in float vFragDepth;"); } src.push("uniform int layerNumber;"); src.push("uniform vec3 coordinateScaler;"); if (clipping) { src.push("in vec4 vWorldPosition;"); src.push("in float vFlags;"); for (let i = 0; i < sectionPlanesState.getNumAllocatedSectionPlanes(); i++) { src.push("uniform bool sectionPlaneActive" + i + ";"); src.push("uniform vec3 sectionPlanePos" + i + ";"); src.push("uniform vec3 sectionPlaneDir" + i + ";"); } } src.push("in highp vec3 relativeToOriginPosition;"); src.push("out highp ivec4 outCoords;"); src.push("void main(void) {"); if (clipping) { src.push(" bool clippable = (int(vFlags) >> 16 & 0xF) == 1;"); src.push(" if (clippable) {"); src.push(" float dist = 0.0;"); for (var i = 0; i < sectionPlanesState.getNumAllocatedSectionPlanes(); i++) { src.push(" if (sectionPlaneActive" + i + ") {"); src.push(" dist += clamp(dot(-sectionPlaneDir" + i + ".xyz, vWorldPosition.xyz - sectionPlanePos" + i + ".xyz), 0.0, 1000.0);"); src.push(" }"); } src.push(" if (dist > 0.0) { discard; }"); src.push(" }"); } { src.push(" gl_FragDepth = isPerspective == 0.0 ? gl_FragCoord.z : log2( vFragDepth ) * logDepthBufFC * 0.5;"); } src.push("outCoords = ivec4(relativeToOriginPosition.xyz*coordinateScaler.xyz, layerNumber);"); src.push("}"); return src; } webglContextRestored() { this._program = null; } destroy() { if (this._program) { this._program.destroy(); } this._program = null; } } /** * @private */ class VBOBatchingPointsRenderers { constructor(scene) { this._scene = scene; } _compile() { if (this._colorRenderer && (!this._colorRenderer.getValid())) { this._colorRenderer.destroy(); this._colorRenderer = null; } if (this._silhouetteRenderer && (!this._silhouetteRenderer.getValid())) { this._silhouetteRenderer.destroy(); this._silhouetteRenderer = null; } if (this._pickMeshRenderer && (!this._pickMeshRenderer.getValid())) { this._pickMeshRenderer.destroy(); this._pickMeshRenderer = null; } if (this._pickDepthRenderer && (!this._pickDepthRenderer.getValid())) { this._pickDepthRenderer.destroy(); this._pickDepthRenderer = null; } if (this._occlusionRenderer && this._occlusionRenderer.getValid() === false) { this._occlusionRenderer.destroy(); this._occlusionRenderer = null; } if (this._snapInitRenderer && (!this._snapInitRenderer.getValid())) { this._snapInitRenderer.destroy(); this._snapInitRenderer = null; } if (this._snapRenderer && (!this._snapRenderer.getValid())) { this._snapRenderer.destroy(); this._snapRenderer = null; } } get colorRenderer() { if (!this._colorRenderer) { this._colorRenderer = new VBOBatchingPointsColorRenderer(this._scene); } return this._colorRenderer; } get silhouetteRenderer() { if (!this._silhouetteRenderer) { this._silhouetteRenderer = new VBOBatchingPointsSilhouetteRenderer(this._scene); } return this._silhouetteRenderer; } get pickMeshRenderer() { if (!this._pickMeshRenderer) { this._pickMeshRenderer = new VBOBatchingPointsPickMeshRenderer(this._scene); } return this._pickMeshRenderer; } get pickDepthRenderer() { if (!this._pickDepthRenderer) { this._pickDepthRenderer = new VBOBatchingPointsPickDepthRenderer(this._scene); } return this._pickDepthRenderer; } get occlusionRenderer() { if (!this._occlusionRenderer) { this._occlusionRenderer = new VBOBatchingPointsOcclusionRenderer(this._scene); } return this._occlusionRenderer; } get snapInitRenderer() { if (!this._snapInitRenderer) { this._snapInitRenderer = new VBOBatchingPointsSnapInitRenderer(this._scene, false); } return this._snapInitRenderer; } get snapRenderer() { if (!this._snapRenderer) { this._snapRenderer = new VBOBatchingPointsSnapRenderer(this._scene); } return this._snapRenderer; } _destroy() { if (this._colorRenderer) { this._colorRenderer.destroy(); } if (this._silhouetteRenderer) { this._silhouetteRenderer.destroy(); } if (this._pickMeshRenderer) { this._pickMeshRenderer.destroy(); } if (this._pickDepthRenderer) { this._pickDepthRenderer.destroy(); } if (this._occlusionRenderer) { this._occlusionRenderer.destroy(); } if (this._snapInitRenderer) { this._snapInitRenderer.destroy(); } if (this._snapRenderer) { this._snapRenderer.destroy(); } } } const cachedRenderers$2 = {}; /** * @private */ function getRenderers$3(scene) { const sceneId = scene.id; let renderers = cachedRenderers$2[sceneId]; if (!renderers) { renderers = new VBOBatchingPointsRenderers(scene); cachedRenderers$2[sceneId] = renderers; renderers._compile(); scene.on("compile", () => { renderers._compile(); }); scene.on("destroyed", () => { delete cachedRenderers$2[sceneId]; renderers._destroy(); }); } return renderers; } /** * @private */ class VBOBatchingPointsLayer { /** * @param model * @param cfg * @param cfg.layerIndex * @param cfg.positionsDecodeMatrix * @param cfg.maxGeometryBatchSize * @param cfg.origin * @param cfg.scratchMemory */ constructor(cfg) { // console.info("Creating VBOBatchingPointsLayer"); /** * Owner model * @type {VBOSceneModel} */ this.model = cfg.model; /** * State sorting key. * @type {string} */ this.sortId = "PointsBatchingLayer"; /** * Index of this PointsBatchingLayer in {@link VBOSceneModel#_layerList}. * @type {Number} */ this.layerIndex = cfg.layerIndex; this._renderers = getRenderers$3(cfg.model.scene); const maxGeometryBatchSize = cfg.maxGeometryBatchSize || 5000000; const attribute = function() { const portions = [ ]; return { append: function(data, times = 1, denormalizeScale = 1.0) { portions.push({ data: data, times: times, denormalizeScale: denormalizeScale }); }, compileBuffer: function(type) { let len = 0; portions.forEach(p => { len += p.times * p.data.length; }); const buf = new type(len); let begin = 0; portions.forEach(p => { const data = p.data; const dScale = p.denormalizeScale; const subBuf = buf.subarray(begin); if (dScale === 1.0) { subBuf.set(data, 0); } else { for (let i = 0; i < data.length; ++i) { subBuf[i] = data[i] * dScale; } } let soFar = data.length; const allDataLen = p.times * data.length; while (soFar < allDataLen) { const toCopy = Math.min(soFar, allDataLen - soFar); subBuf.set(subBuf.subarray(0, toCopy), soFar); soFar += toCopy; } begin += soFar; }); return buf; } }; }; this._buffer = { maxVerts: maxGeometryBatchSize, positions: attribute(), colors: attribute(), pickColors: attribute(), vertsIndex: 0 }; this._scratchMemory = cfg.scratchMemory; this._state = new RenderState({ positionsBuf: null, offsetsBuf: null, colorsBuf: null, flagsBuf: null, positionsDecodeMatrix: math.mat4(), origin: null }); // These counts are used to avoid unnecessary render passes this._numPortions = 0; this._numVisibleLayerPortions = 0; this._numTransparentLayerPortions = 0; this._numXRayedLayerPortions = 0; this._numSelectedLayerPortions = 0; this._numHighlightedLayerPortions = 0; this._numClippableLayerPortions = 0; this._numPickableLayerPortions = 0; this._numCulledLayerPortions = 0; this._modelAABB = math.collapseAABB3(); // Model-space AABB this._portions = []; this._meshes = []; this._aabb = math.collapseAABB3(); this.aabbDirty = true; this._finalized = false; if (cfg.positionsDecodeMatrix) { this._state.positionsDecodeMatrix.set(cfg.positionsDecodeMatrix); this._preCompressedPositionsExpected = true; } else { this._preCompressedPositionsExpected = false; } if (cfg.origin) { this._state.origin = math.vec3(cfg.origin); } } get aabb() { if (this.aabbDirty) { math.collapseAABB3(this._aabb); for (let i = 0, len = this._meshes.length; i < len; i++) { math.expandAABB3(this._aabb, this._meshes[i].aabb); } this.aabbDirty = false; } return this._aabb; } /** * Tests if there is room for another portion in this PointsBatchingLayer. * * @param lenPositions Number of positions we'd like to create in the portion. * @returns {Boolean} True if OK to create another portion. */ canCreatePortion(lenPositions) { if (this._finalized) { throw "Already finalized"; } return (this._buffer.vertsIndex + (lenPositions / 3)) <= this._buffer.maxVerts; } /** * Creates a new portion within this PointsBatchingLayer, returns the new portion ID. * * Gives the portion the specified geometry, color and matrix. * * @param mesh The SceneModelMesh that owns the portion * @param cfg.positions Flat float Local-space positions array. * @param cfg.positionsCompressed Flat quantized positions array - decompressed with PointsBatchingLayer positionsDecodeMatrix * @param [cfg.colorsCompressed] Quantized RGB colors [0..255,0..255,0..255,0..255] * @param [cfg.colors] Flat float colors array. * @param cfg.color Float RGB color [0..1,0..1,0..1] * @param [cfg.meshMatrix] Flat float 4x4 matrix * @param cfg.aabb Flat float AABB World-space AABB * @param cfg.pickColor Quantized pick color * @returns {number} Portion ID */ createPortion(mesh, cfg) { if (this._finalized) { throw "Already finalized"; } const buffer = this._buffer; const positions = this._preCompressedPositionsExpected ? cfg.positionsCompressed : cfg.positions; if (! positions) { throw ((this._preCompressedPositionsExpected ? "positionsCompressed" : "positions") + " expected"); } buffer.positions.append(positions); const numVerts = positions.length / 3; const color = cfg.color; const colorsCompressed = cfg.colorsCompressed; const colors = cfg.colors; const pickColor = cfg.pickColor; if (colorsCompressed) { buffer.colors.append(colorsCompressed); } else if (colors) { buffer.colors.append(colors, 1, 255.0); } else if (color) { // Color is pre-quantized by VBOSceneModel buffer.colors.append([ color[0], color[1], color[2], 1.0 ], numVerts); } buffer.pickColors.append(pickColor.slice(0, 4), numVerts); math.expandAABB3(this._modelAABB, cfg.aabb); const portionId = this._portions.length / 2; this._portions.push(this._buffer.vertsIndex); this._portions.push(numVerts); this._buffer.vertsIndex += numVerts; this._numPortions++; this.model.numPortions++; this._meshes.push(mesh); return portionId; } /** * Builds batch VBOs from appended geometries. * No more portions can then be created. */ finalize() { if (this._finalized) { return; } const state = this._state; const gl = this.model.scene.canvas.gl; const buffer = this._buffer; const maybeCreateGlBuffer = (srcData, size, usage) => (srcData.length > 0) ? new ArrayBuf(gl, gl.ARRAY_BUFFER, srcData, srcData.length, size, usage) : null; const positions = (this._preCompressedPositionsExpected ? buffer.positions.compileBuffer(Uint16Array) : (quantizePositions(buffer.positions.compileBuffer(Float32Array), this._modelAABB, state.positionsDecodeMatrix))); state.positionsBuf = maybeCreateGlBuffer(positions, 3, gl.STATIC_DRAW); state.flagsBuf = maybeCreateGlBuffer(new Float32Array(this._buffer.vertsIndex), 1, gl.DYNAMIC_DRAW); // Because we build flags arrays here, get their length from the positions array state.colorsBuf = maybeCreateGlBuffer(buffer.colors.compileBuffer(Uint8Array), 4, gl.STATIC_DRAW); state.pickColorsBuf = maybeCreateGlBuffer(buffer.pickColors.compileBuffer(Uint8Array), 4, gl.STATIC_DRAW); state.offsetsBuf = this.model.scene.entityOffsetsEnabled ? maybeCreateGlBuffer(new Float32Array(this._buffer.vertsIndex * 3), 3, gl.DYNAMIC_DRAW) : null; this._buffer = null; this._finalized = true; } initFlags(portionId, flags, meshTransparent) { if (flags & ENTITY_FLAGS.VISIBLE) { this._numVisibleLayerPortions++; this.model.numVisibleLayerPortions++; } if (flags & ENTITY_FLAGS.HIGHLIGHTED) { this._numHighlightedLayerPortions++; this.model.numHighlightedLayerPortions++; } if (flags & ENTITY_FLAGS.XRAYED) { this._numXRayedLayerPortions++; this.model.numXRayedLayerPortions++; } if (flags & ENTITY_FLAGS.SELECTED) { this._numSelectedLayerPortions++; this.model.numSelectedLayerPortions++; } if (flags & ENTITY_FLAGS.CLIPPABLE) { this._numClippableLayerPortions++; this.model.numClippableLayerPortions++; } if (flags & ENTITY_FLAGS.PICKABLE) { this._numPickableLayerPortions++; this.model.numPickableLayerPortions++; } if (flags & ENTITY_FLAGS.CULLED) { this._numCulledLayerPortions++; this.model.numCulledLayerPortions++; } if (meshTransparent) { this._numTransparentLayerPortions++; this.model.numTransparentLayerPortions++; } this._setFlags(portionId, flags, meshTransparent); } setVisible(portionId, flags, transparent) { if (!this._finalized) { throw "Not finalized"; } if (flags & ENTITY_FLAGS.VISIBLE) { this._numVisibleLayerPortions++; this.model.numVisibleLayerPortions++; } else { this._numVisibleLayerPortions--; this.model.numVisibleLayerPortions--; } this._setFlags(portionId, flags, transparent); } setHighlighted(portionId, flags, transparent) { if (!this._finalized) { throw "Not finalized"; } if (flags & ENTITY_FLAGS.HIGHLIGHTED) { this._numHighlightedLayerPortions++; this.model.numHighlightedLayerPortions++; } else { this._numHighlightedLayerPortions--; this.model.numHighlightedLayerPortions--; } this._setFlags(portionId, flags, transparent); } setXRayed(portionId, flags, transparent) { if (!this._finalized) { throw "Not finalized"; } if (flags & ENTITY_FLAGS.XRAYED) { this._numXRayedLayerPortions++; this.model.numXRayedLayerPortions++; } else { this._numXRayedLayerPortions--; this.model.numXRayedLayerPortions--; } this._setFlags(portionId, flags, transparent); } setSelected(portionId, flags, transparent) { if (!this._finalized) { throw "Not finalized"; } if (flags & ENTITY_FLAGS.SELECTED) { this._numSelectedLayerPortions++; this.model.numSelectedLayerPortions++; } else { this._numSelectedLayerPortions--; this.model.numSelectedLayerPortions--; } this._setFlags(portionId, flags, transparent); } setEdges(portionId, flags, transparent) { if (!this._finalized) { throw "Not finalized"; } // Not applicable to point clouds } setClippable(portionId, flags) { if (!this._finalized) { throw "Not finalized"; } if (flags & ENTITY_FLAGS.CLIPPABLE) { this._numClippableLayerPortions++; this.model.numClippableLayerPortions++; } else { this._numClippableLayerPortions--; this.model.numClippableLayerPortions--; } this._setFlags(portionId, flags); } setCulled(portionId, flags, transparent) { if (!this._finalized) { throw "Not finalized"; } if (flags & ENTITY_FLAGS.CULLED) { this._numCulledLayerPortions++; this.model.numCulledLayerPortions++; } else { this._numCulledLayerPortions--; this.model.numCulledLayerPortions--; } this._setFlags(portionId, flags, transparent); } setCollidable(portionId, flags) { if (!this._finalized) { throw "Not finalized"; } } setPickable(portionId, flags, transparent) { if (!this._finalized) { throw "Not finalized"; } if (flags & ENTITY_FLAGS.PICKABLE) { this._numPickableLayerPortions++; this.model.numPickableLayerPortions++; } else { this._numPickableLayerPortions--; this.model.numPickableLayerPortions--; } this._setFlags(portionId, flags, transparent); } setColor(portionId, color) { if (!this._finalized) { throw "Not finalized"; } const portionsIdx = portionId * 2; const vertexBase = this._portions[portionsIdx]; const numVerts = this._portions[portionsIdx + 1]; const firstColor = vertexBase * 4; const lenColor = numVerts * 4; const tempArray = this._scratchMemory.getUInt8Array(lenColor); const r = color[0]; const g = color[1]; const b = color[2]; for (let i = 0; i < lenColor; i += 4) { tempArray[i + 0] = r; tempArray[i + 1] = g; tempArray[i + 2] = b; } this._state.colorsBuf.setData(tempArray, firstColor, lenColor); } setTransparent(portionId, flags, transparent) { if (transparent) { this._numTransparentLayerPortions++; this.model.numTransparentLayerPortions++; } else { this._numTransparentLayerPortions--; this.model.numTransparentLayerPortions--; } this._setFlags(portionId, flags, transparent); } /** * flags are 4bits values encoded on a 32bit base. color flag on the first 4 bits, silhouette flag on the next 4 bits and so on for edge, pick and clippable. */ _setFlags(portionId, flags, transparent) { if (!this._finalized) { throw "Not finalized"; } const portionsIdx = portionId * 2; const vertexBase = this._portions[portionsIdx]; const numVerts = this._portions[portionsIdx + 1]; const firstFlag = vertexBase; const lenFlags = numVerts; const tempArray = this._scratchMemory.getFloat32Array(lenFlags); const visible = !!(flags & ENTITY_FLAGS.VISIBLE); const xrayed = !!(flags & ENTITY_FLAGS.XRAYED); const highlighted = !!(flags & ENTITY_FLAGS.HIGHLIGHTED); const selected = !!(flags & ENTITY_FLAGS.SELECTED); const pickable = !!(flags & ENTITY_FLAGS.PICKABLE); const culled = !!(flags & ENTITY_FLAGS.CULLED); let colorFlag; if (!visible || culled || xrayed || (highlighted && !this.model.scene.highlightMaterial.glowThrough) || (selected && !this.model.scene.selectedMaterial.glowThrough)) { colorFlag = RENDER_PASSES.NOT_RENDERED; } else { if (transparent) { colorFlag = RENDER_PASSES.COLOR_TRANSPARENT; } else { colorFlag = RENDER_PASSES.COLOR_OPAQUE; } } let silhouetteFlag; if (!visible || culled) { silhouetteFlag = RENDER_PASSES.NOT_RENDERED; } else if (selected) { silhouetteFlag = RENDER_PASSES.SILHOUETTE_SELECTED; } else if (highlighted) { silhouetteFlag = RENDER_PASSES.SILHOUETTE_HIGHLIGHTED; } else if (xrayed) { silhouetteFlag = RENDER_PASSES.SILHOUETTE_XRAYED; } else { silhouetteFlag = RENDER_PASSES.NOT_RENDERED; } let pickFlag = (visible && !culled && pickable) ? RENDER_PASSES.PICK : RENDER_PASSES.NOT_RENDERED; const clippableFlag = !!(flags & ENTITY_FLAGS.CLIPPABLE) ? 1 : 0; for (let i = 0; i < lenFlags; i++) { let vertFlag = 0; vertFlag |= colorFlag; vertFlag |= silhouetteFlag << 4; // no edges vertFlag |= pickFlag << 12; vertFlag |= clippableFlag << 16; tempArray[i] = vertFlag; } this._state.flagsBuf.setData(tempArray, firstFlag); } setOffset(portionId, offset) { if (!this._finalized) { throw "Not finalized"; } if (!this.model.scene.entityOffsetsEnabled) { this.model.error("Entity#offset not enabled for this Viewer"); // See Viewer entityOffsetsEnabled return; } const portionsIdx = portionId * 2; const vertexBase = this._portions[portionsIdx]; const numVerts = this._portions[portionsIdx + 1]; const firstOffset = vertexBase * 3; const lenOffsets = numVerts * 3; const tempArray = this._scratchMemory.getFloat32Array(lenOffsets); const x = offset[0]; const y = offset[1]; const z = offset[2]; for (let i = 0; i < lenOffsets; i += 3) { tempArray[i + 0] = x; tempArray[i + 1] = y; tempArray[i + 2] = z; } this._state.offsetsBuf.setData(tempArray, firstOffset, lenOffsets); } //-- NORMAL RENDERING ---------------------------------------------------------------------------------------------- drawColorOpaque(renderFlags, frameCtx) { if (this._numCulledLayerPortions === this._numPortions || this._numVisibleLayerPortions === 0 || this._numTransparentLayerPortions === this._numPortions || this._numXRayedLayerPortions === this._numPortions) { return; } if (this._renderers.colorRenderer) { this._renderers.colorRenderer.drawLayer(frameCtx, this, RENDER_PASSES.COLOR_OPAQUE); } } drawColorTransparent(renderFlags, frameCtx) { if (this._numCulledLayerPortions === this._numPortions || this._numVisibleLayerPortions === 0 || this._numTransparentLayerPortions === 0 || this._numXRayedLayerPortions === this._numPortions) { return; } if (this._renderers.colorRenderer) { this._renderers.colorRenderer.drawLayer(frameCtx, this, RENDER_PASSES.COLOR_TRANSPARENT); } } // -- RENDERING SAO POST EFFECT TARGETS ---------------------------------------------------------------------------- drawDepth(renderFlags, frameCtx) { } drawNormals(renderFlags, frameCtx) { } // -- EMPHASIS RENDERING ------------------------------------------------------------------------------------------- drawSilhouetteXRayed(renderFlags, frameCtx) { if (this._numCulledLayerPortions === this._numPortions || this._numVisibleLayerPortions === 0 || this._numXRayedLayerPortions === 0) { return; } if (this._renderers.silhouetteRenderer) { this._renderers.silhouetteRenderer.drawLayer(frameCtx, this, RENDER_PASSES.SILHOUETTE_XRAYED); } } drawSilhouetteHighlighted(renderFlags, frameCtx) { if (this._numCulledLayerPortions === this._numPortions || this._numVisibleLayerPortions === 0 || this._numHighlightedLayerPortions === 0) { return; } if (this._renderers.silhouetteRenderer) { this._renderers.silhouetteRenderer.drawLayer(frameCtx, this, RENDER_PASSES.SILHOUETTE_HIGHLIGHTED); } } drawSilhouetteSelected(renderFlags, frameCtx) { if (this._numCulledLayerPortions === this._numPortions || this._numVisibleLayerPortions === 0 || this._numSelectedLayerPortions === 0) { return; } if (this._renderers.silhouetteRenderer) { this._renderers.silhouetteRenderer.drawLayer(frameCtx, this, RENDER_PASSES.SILHOUETTE_SELECTED); } } //-- EDGES RENDERING ----------------------------------------------------------------------------------------------- drawEdgesColorOpaque(renderFlags, frameCtx) { } drawEdgesColorTransparent(renderFlags, frameCtx) { } drawEdgesHighlighted(renderFlags, frameCtx) { } drawEdgesSelected(renderFlags, frameCtx) { } drawEdgesXRayed(renderFlags, frameCtx) { } //---- PICKING ---------------------------------------------------------------------------------------------------- drawPickMesh(renderFlags, frameCtx) { if (this._numCulledLayerPortions === this._numPortions || this._numVisibleLayerPortions === 0) { return; } if (this._renderers.pickMeshRenderer) { this._renderers.pickMeshRenderer.drawLayer(frameCtx, this, RENDER_PASSES.PICK); } } drawPickDepths(renderFlags, frameCtx) { if (this._numCulledLayerPortions === this._numPortions || this._numVisibleLayerPortions === 0) { return; } if (this._renderers.pickDepthRenderer) { this._renderers.pickDepthRenderer.drawLayer(frameCtx, this, RENDER_PASSES.PICK); } } drawPickNormals(renderFlags, frameCtx) { } drawSnapInit(renderFlags, frameCtx) { if (this._numCulledLayerPortions === this._numPortions || this._numVisibleLayerPortions === 0) { return; } if (this._renderers.snapInitRenderer) { this._renderers.snapInitRenderer.drawLayer(frameCtx, this, RENDER_PASSES.PICK); } } drawSnap(renderFlags, frameCtx) { if (this._numCulledLayerPortions === this._numPortions || this._numVisibleLayerPortions === 0) { return; } if (this._renderers.snapRenderer) { this._renderers.snapRenderer.drawLayer(frameCtx, this, RENDER_PASSES.PICK); } } //---- OCCLUSION TESTING ------------------------------------------------------------------------------------------- drawOcclusion(renderFlags, frameCtx) { if (this._numCulledLayerPortions === this._numPortions || this._numVisibleLayerPortions === 0) { return; } if (this._renderers.occlusionRenderer) { this._renderers.occlusionRenderer.drawLayer(frameCtx, this, RENDER_PASSES.COLOR_OPAQUE); } } //---- SHADOWS ----------------------------------------------------------------------------------------------------- drawShadow(renderFlags, frameCtx) { } destroy() { const state = this._state; if (state.positionsBuf) { state.positionsBuf.destroy(); state.positionsBuf = null; } if (state.offsetsBuf) { state.offsetsBuf.destroy(); state.offsetsBuf = null; } if (state.colorsBuf) { state.colorsBuf.destroy(); state.colorsBuf = null; } if (state.flagsBuf) { state.flagsBuf.destroy(); state.flagsBuf = null; } if (state.pickColorsBuf) { state.pickColorsBuf.destroy(); state.pickColorsBuf = null; } state.destroy(); } } /** * @private */ class VBOInstancingPointsRenderer extends VBORenderer { constructor(scene, withSAO) { super(scene, withSAO, {instancing: true}); } _draw(drawCfg) { const {gl} = this._scene.canvas; const { state, frameCtx, incrementDrawState, } = drawCfg; gl.drawArraysInstanced(gl.POINTS, 0, state.positionsBuf.numItems, state.numInstances); if (incrementDrawState) { frameCtx.drawArrays++; } } } /** * @private */ class VBOInstancingPointsColorRenderer extends VBOInstancingPointsRenderer { _getHash() { return this._scene._sectionPlanesState.getHash() + this._scene.pointsMaterial.hash; } drawLayer(frameCtx, layer, renderPass) { super.drawLayer(frameCtx, layer, renderPass, { incrementDrawState: true }); } _buildVertexShader() { const scene = this._scene; const sectionPlanesState = scene._sectionPlanesState; const clipping = sectionPlanesState.getNumAllocatedSectionPlanes() > 0; const pointsMaterial = scene.pointsMaterial._state; const src = []; src.push('#version 300 es'); src.push("// Points instancing color vertex shader"); src.push("uniform int renderPass;"); src.push("in vec3 position;"); src.push("in vec4 color;"); src.push("in float flags;"); if (scene.entityOffsetsEnabled) { src.push("in vec3 offset;"); } src.push("in vec4 modelMatrixCol0;"); // Modeling matrix src.push("in vec4 modelMatrixCol1;"); src.push("in vec4 modelMatrixCol2;"); this._addMatricesUniformBlockLines(src); src.push("uniform float pointSize;"); if (pointsMaterial.perspectivePoints) { src.push("uniform float nearPlaneHeight;"); } if (pointsMaterial.filterIntensity) { src.push("uniform vec2 intensityRange;"); } if (scene.logarithmicDepthBufferEnabled) { src.push("uniform float logDepthBufFC;"); src.push("out float vFragDepth;"); } if (clipping) { src.push("out vec4 vWorldPosition;"); src.push("out float vFlags;"); } src.push("out vec4 vColor;"); src.push("void main(void) {"); // colorFlag = NOT_RENDERED | COLOR_OPAQUE | COLOR_TRANSPARENT // renderPass = COLOR_OPAQUE src.push(`int colorFlag = int(flags) & 0xF;`); src.push(`if (colorFlag != renderPass) {`); src.push(" gl_Position = vec4(0.0, 0.0, 0.0, 0.0);"); // Cull vertex src.push("} else {"); if (pointsMaterial.filterIntensity) { src.push("float intensity = float(color.a) / 255.0;"); src.push("if (intensity < intensityRange[0] || intensity > intensityRange[1]) {"); src.push(" gl_Position = vec4(0.0, 0.0, 0.0, 0.0);"); // Cull vertex src.push("} else {"); } src.push("vec4 worldPosition = positionsDecodeMatrix * vec4(position, 1.0); "); src.push("worldPosition = worldMatrix * vec4(dot(worldPosition, modelMatrixCol0), dot(worldPosition, modelMatrixCol1), dot(worldPosition, modelMatrixCol2), 1.0);"); if (scene.entityOffsetsEnabled) { src.push(" worldPosition.xyz = worldPosition.xyz + offset;"); } src.push("vec4 viewPosition = viewMatrix * worldPosition; "); src.push("vColor = vec4(float(color.r) / 255.0, float(color.g) / 255.0, float(color.b) / 255.0, 1.0);"); if (clipping) { src.push("vWorldPosition = worldPosition;"); src.push("vFlags = flags;"); } src.push("vec4 clipPos = projMatrix * viewPosition;"); if (scene.logarithmicDepthBufferEnabled) { src.push("vFragDepth = 1.0 + clipPos.w;"); } src.push("gl_Position = clipPos;"); if (pointsMaterial.perspectivePoints) { src.push("gl_PointSize = (nearPlaneHeight * pointSize) / clipPos.w;"); src.push("gl_PointSize = max(gl_PointSize, " + Math.floor(pointsMaterial.minPerspectivePointSize) + ".0);"); src.push("gl_PointSize = min(gl_PointSize, " + Math.floor(pointsMaterial.maxPerspectivePointSize) + ".0);"); } else { src.push("gl_PointSize = pointSize;"); } src.push("}"); if (pointsMaterial.filterIntensity) { src.push("}"); } src.push("}"); return src; } _buildFragmentShader() { const scene = this._scene; const sectionPlanesState = scene._sectionPlanesState; const clipping = sectionPlanesState.getNumAllocatedSectionPlanes() > 0; const src = []; src.push('#version 300 es'); src.push("// Points instancing color fragment shader"); src.push("#ifdef GL_FRAGMENT_PRECISION_HIGH"); src.push("precision highp float;"); src.push("precision highp int;"); src.push("#else"); src.push("precision mediump float;"); src.push("precision mediump int;"); src.push("#endif"); if (scene.logarithmicDepthBufferEnabled) { src.push("uniform float logDepthBufFC;"); src.push("in float vFragDepth;"); } if (clipping) { src.push("in vec4 vWorldPosition;"); src.push("in float vFlags;"); for (let i = 0, len = sectionPlanesState.getNumAllocatedSectionPlanes(); i < len; i++) { src.push("uniform bool sectionPlaneActive" + i + ";"); src.push("uniform vec3 sectionPlanePos" + i + ";"); src.push("uniform vec3 sectionPlaneDir" + i + ";"); } } src.push("in vec4 vColor;"); src.push("out vec4 outColor;"); src.push("void main(void) {"); if (scene.pointsMaterial.roundPoints) { src.push(" vec2 cxy = 2.0 * gl_PointCoord - 1.0;"); src.push(" float r = dot(cxy, cxy);"); src.push(" if (r > 1.0) {"); src.push(" discard;"); src.push(" }"); } if (clipping) { src.push(" bool clippable = (int(vFlags) >> 16 & 0xF) == 1;"); src.push(" if (clippable) {"); src.push(" float dist = 0.0;"); for (let i = 0, len = sectionPlanesState.getNumAllocatedSectionPlanes(); i < len; i++) { src.push("if (sectionPlaneActive" + i + ") {"); src.push(" dist += clamp(dot(-sectionPlaneDir" + i + ".xyz, vWorldPosition.xyz - sectionPlanePos" + i + ".xyz), 0.0, 1000.0);"); src.push("}"); } src.push("if (dist > 0.0) { discard; }"); src.push("}"); } src.push(" outColor = vColor;"); if (scene.logarithmicDepthBufferEnabled) { src.push("gl_FragDepth = log2( vFragDepth ) * logDepthBufFC * 0.5;"); } src.push("}"); return src; } } /** * @private */ class VBOInstancingPointsSilhouetteRenderer extends VBOInstancingPointsRenderer { _getHash() { return this._scene._sectionPlanesState.getHash() + this._scene.pointsMaterial.hash; } drawLayer(frameCtx, instancingLayer, renderPass) { super.drawLayer(frameCtx, instancingLayer, renderPass, {colorUniform: true}); } _buildVertexShader() { const scene = this._scene; const sectionPlanesState = scene._sectionPlanesState; const clipping = sectionPlanesState.getNumAllocatedSectionPlanes() > 0; const pointsMaterial = scene.pointsMaterial._state; const src = []; src.push('#version 300 es'); src.push("// Points instancing silhouette vertex shader"); src.push("uniform int renderPass;"); src.push("in vec3 position;"); if (scene.entityOffsetsEnabled) { src.push("in vec3 offset;"); } src.push("in float flags;"); src.push("in vec4 color;"); src.push("in vec4 modelMatrixCol0;"); // Modeling matrix src.push("in vec4 modelMatrixCol1;"); src.push("in vec4 modelMatrixCol2;"); this._addMatricesUniformBlockLines(src); src.push("uniform float pointSize;"); if (pointsMaterial.perspectivePoints) { src.push("uniform float nearPlaneHeight;"); } if (scene.logarithmicDepthBufferEnabled) { src.push("uniform float logDepthBufFC;"); src.push("out float vFragDepth;"); } src.push("uniform vec4 silhouetteColor;"); if (clipping) { src.push("out vec4 vWorldPosition;"); src.push("out float vFlags;"); } src.push("out vec4 vColor;"); src.push("void main(void) {"); // silhouetteFlag = NOT_RENDERED | SILHOUETTE_HIGHLIGHTED | SILHOUETTE_SELECTED | | SILHOUETTE_XRAYED // renderPass = SILHOUETTE_HIGHLIGHTED | SILHOUETTE_SELECTED | | SILHOUETTE_XRAYED src.push(`int silhouetteFlag = int(flags) >> 4 & 0xF;`); src.push(`if (silhouetteFlag != renderPass) {`); src.push(" gl_Position = vec4(0.0, 0.0, 0.0, 0.0);"); // Cull vertex src.push("} else {"); src.push("vec4 worldPosition = positionsDecodeMatrix * vec4(position, 1.0); "); src.push("worldPosition = worldMatrix * vec4(dot(worldPosition, modelMatrixCol0), dot(worldPosition, modelMatrixCol1), dot(worldPosition, modelMatrixCol2), 1.0);"); if (scene.entityOffsetsEnabled) { src.push(" worldPosition.xyz = worldPosition.xyz + offset;"); } src.push("vec4 viewPosition = viewMatrix * worldPosition; "); if (clipping) { src.push("vWorldPosition = worldPosition;"); src.push("vFlags = flags;"); } src.push("vec4 clipPos = projMatrix * viewPosition;"); if (scene.logarithmicDepthBufferEnabled) { src.push("vFragDepth = 1.0 + clipPos.w;"); } src.push("vColor = vec4(float(silhouetteColor.r) / 255.0, float(silhouetteColor.g) / 255.0, float(silhouetteColor.b) / 255.0, float(color.a) / 255.0);"); src.push("gl_Position = clipPos;"); if (pointsMaterial.perspectivePoints) { src.push("gl_PointSize = (nearPlaneHeight * pointSize) / clipPos.w;"); src.push("gl_PointSize = max(gl_PointSize, " + Math.floor(pointsMaterial.minPerspectivePointSize) + ".0);"); src.push("gl_PointSize = min(gl_PointSize, " + Math.floor(pointsMaterial.maxPerspectivePointSize) + ".0);"); } else { src.push("gl_PointSize = pointSize;"); } src.push("}"); src.push("}"); return src; } _buildFragmentShader() { const scene = this._scene; const sectionPlanesState = scene._sectionPlanesState; const clipping = sectionPlanesState.getNumAllocatedSectionPlanes() > 0; const src = []; src.push('#version 300 es'); src.push("// Points instancing silhouette fragment shader"); src.push("#ifdef GL_FRAGMENT_PRECISION_HIGH"); src.push("precision highp float;"); src.push("precision highp int;"); src.push("#else"); src.push("precision mediump float;"); src.push("precision mediump int;"); src.push("#endif"); if (scene.logarithmicDepthBufferEnabled) { src.push("uniform float logDepthBufFC;"); src.push("in float vFragDepth;"); } if (clipping) { src.push("in vec4 vWorldPosition;"); src.push("in float vFlags;"); for (let i = 0, len = sectionPlanesState.getNumAllocatedSectionPlanes(); i < len; i++) { src.push("uniform bool sectionPlaneActive" + i + ";"); src.push("uniform vec3 sectionPlanePos" + i + ";"); src.push("uniform vec3 sectionPlaneDir" + i + ";"); } } src.push("in vec4 vColor;"); src.push("out vec4 outColor;"); src.push("void main(void) {"); if (scene.pointsMaterial.roundPoints) { src.push(" vec2 cxy = 2.0 * gl_PointCoord - 1.0;"); src.push(" float r = dot(cxy, cxy);"); src.push(" if (r > 1.0) {"); src.push(" discard;"); src.push(" }"); } if (clipping) { src.push(" bool clippable = (int(vFlags) >> 16 & 0xF) == 1;"); src.push(" if (clippable) {"); src.push(" float dist = 0.0;"); for (let i = 0, len = sectionPlanesState.getNumAllocatedSectionPlanes(); i < len; i++) { src.push("if (sectionPlaneActive" + i + ") {"); src.push(" dist += clamp(dot(-sectionPlaneDir" + i + ".xyz, vWorldPosition.xyz - sectionPlanePos" + i + ".xyz), 0.0, 1000.0);"); src.push("}"); } src.push("if (dist > 0.0) { discard; }"); src.push("}"); } if (scene.logarithmicDepthBufferEnabled) { src.push("gl_FragDepth = log2( vFragDepth ) * logDepthBufFC * 0.5;"); } src.push("outColor = vColor;"); src.push("}"); return src; } } /** * @private */ class VBOInstancingPointsPickMeshRenderer extends VBOInstancingPointsRenderer { _getHash() { return this._scene._sectionPlanesState.getHash() + this._scene.pointsMaterial.hash; } _buildVertexShader() { const scene = this._scene; const sectionPlanesState = scene._sectionPlanesState; const clipping = sectionPlanesState.getNumAllocatedSectionPlanes() > 0; const pointsMaterial = scene.pointsMaterial._state; const src = []; src.push ('#version 300 es'); src.push("// Points instancing pick mesh vertex shader"); src.push("uniform int renderPass;"); src.push("in vec3 position;"); if (scene.entityOffsetsEnabled) { src.push("in vec3 offset;"); } src.push("in float flags;"); src.push("in vec4 pickColor;"); src.push("in vec4 modelMatrixCol0;"); // Modeling matrix src.push("in vec4 modelMatrixCol1;"); src.push("in vec4 modelMatrixCol2;"); src.push("uniform bool pickInvisible;"); this._addMatricesUniformBlockLines(src); this._addRemapClipPosLines(src); src.push("uniform float pointSize;"); if (pointsMaterial.perspectivePoints) { src.push("uniform float nearPlaneHeight;"); } if (scene.logarithmicDepthBufferEnabled) { src.push("uniform float logDepthBufFC;"); src.push("out float vFragDepth;"); } if (clipping) { src.push("out vec4 vWorldPosition;"); src.push("out float vFlags;"); } src.push("out vec4 vPickColor;"); src.push("void main(void) {"); // pickFlag = NOT_RENDERED | PICK // renderPass = PICK src.push(`int pickFlag = int(flags) >> 12 & 0xF;`); src.push(`if (pickFlag != renderPass) {`); src.push(" gl_Position = vec4(0.0, 0.0, 0.0, 0.0);"); // Cull vertex src.push("} else {"); src.push(" vec4 worldPosition = positionsDecodeMatrix * vec4(position, 1.0); "); src.push(" worldPosition = worldMatrix * vec4(dot(worldPosition, modelMatrixCol0), dot(worldPosition, modelMatrixCol1), dot(worldPosition, modelMatrixCol2), 1.0);"); if (scene.entityOffsetsEnabled) { src.push(" worldPosition.xyz = worldPosition.xyz + offset;"); } src.push(" vec4 viewPosition = viewMatrix * worldPosition; "); src.push(" vPickColor = vec4(float(pickColor.r) / 255.0, float(pickColor.g) / 255.0, float(pickColor.b) / 255.0, float(pickColor.a) / 255.0);"); if (clipping) { src.push(" vWorldPosition = worldPosition;"); src.push(" vFlags = flags;"); } src.push("vec4 clipPos = projMatrix * viewPosition;"); src.push("gl_Position = remapClipPos(clipPos);"); if (scene.logarithmicDepthBufferEnabled) { src.push("vFragDepth = 1.0 + clipPos.w;"); } if (pointsMaterial.perspectivePoints) { src.push("gl_PointSize = (nearPlaneHeight * pointSize) / clipPos.w;"); src.push("gl_PointSize = max(gl_PointSize, " + Math.floor(pointsMaterial.minPerspectivePointSize) + ".0);"); src.push("gl_PointSize = min(gl_PointSize, " + Math.floor(pointsMaterial.maxPerspectivePointSize) + ".0);"); } else { src.push("gl_PointSize = pointSize;"); } src.push("}"); src.push("}"); return src; } _buildFragmentShader() { const scene = this._scene; const sectionPlanesState = scene._sectionPlanesState; const clipping = sectionPlanesState.getNumAllocatedSectionPlanes() > 0; const src = []; src.push ('#version 300 es'); src.push("// Points instancing pick mesh fragment shader"); src.push("#ifdef GL_FRAGMENT_PRECISION_HIGH"); src.push("precision highp float;"); src.push("precision highp int;"); src.push("#else"); src.push("precision mediump float;"); src.push("precision mediump int;"); src.push("#endif"); if (scene.logarithmicDepthBufferEnabled) { src.push("uniform float logDepthBufFC;"); src.push("in float vFragDepth;"); } if (clipping) { src.push("in vec4 vWorldPosition;"); src.push("in float vFlags;"); for (let i = 0; i < sectionPlanesState.getNumAllocatedSectionPlanes(); i++) { src.push("uniform bool sectionPlaneActive" + i + ";"); src.push("uniform vec3 sectionPlanePos" + i + ";"); src.push("uniform vec3 sectionPlaneDir" + i + ";"); } } src.push("in vec4 vPickColor;"); src.push("out vec4 outColor;"); src.push("void main(void) {"); if (scene.pointsMaterial.roundPoints) { src.push(" vec2 cxy = 2.0 * gl_PointCoord - 1.0;"); src.push(" float r = dot(cxy, cxy);"); src.push(" if (r > 1.0) {"); src.push(" discard;"); src.push(" }"); } if (clipping) { src.push(" bool clippable = (int(vFlags) >> 16 & 0xF) == 1;"); src.push(" if (clippable) {"); src.push(" float dist = 0.0;"); for (let i = 0; i < sectionPlanesState.getNumAllocatedSectionPlanes(); i++) { src.push("if (sectionPlaneActive" + i + ") {"); src.push(" dist += clamp(dot(-sectionPlaneDir" + i + ".xyz, vWorldPosition.xyz - sectionPlanePos" + i + ".xyz), 0.0, 1000.0);"); src.push("}"); } src.push("if (dist > 0.0) { discard; }"); src.push("}"); } if (scene.logarithmicDepthBufferEnabled) { src.push("gl_FragDepth = log2( vFragDepth ) * logDepthBufFC * 0.5;"); } src.push("outColor = vPickColor; "); src.push("}"); return src; } } /** * @private */ class VBOInstancingPointsPickDepthRenderer extends VBOInstancingPointsRenderer { _getHash() { return this._scene._sectionPlanesState.getHash() + this._scene.pointsMaterial.hash; } _buildVertexShader() { const scene = this._scene; const sectionPlanesState = scene._sectionPlanesState; const clipping = sectionPlanesState.getNumAllocatedSectionPlanes() > 0; const pointsMaterial = scene.pointsMaterial._state; const src = []; src.push('#version 300 es'); src.push("// Points instancing pick depth vertex shader"); src.push("uniform int renderPass;"); src.push("in vec3 position;"); if (scene.entityOffsetsEnabled) { src.push("in vec3 offset;"); } src.push("in float flags;"); src.push("in vec4 modelMatrixCol0;"); // Modeling matrix src.push("in vec4 modelMatrixCol1;"); src.push("in vec4 modelMatrixCol2;"); src.push("uniform bool pickInvisible;"); this._addMatricesUniformBlockLines(src); this._addRemapClipPosLines(src); src.push("uniform float pointSize;"); if (pointsMaterial.perspectivePoints) { src.push("uniform float nearPlaneHeight;"); } if (scene.logarithmicDepthBufferEnabled) { src.push("uniform float logDepthBufFC;"); src.push("out float vFragDepth;"); } if (clipping) { src.push("out vec4 vWorldPosition;"); src.push("out float vFlags;"); } src.push("out vec4 vViewPosition;"); src.push("void main(void) {"); // pickFlag = NOT_RENDERED | PICK // renderPass = PICK src.push(`int pickFlag = int(flags) >> 12 & 0xF;`); src.push(`if (pickFlag != renderPass) {`); src.push(" gl_Position = vec4(0.0, 0.0, 0.0, 0.0);"); // Cull vertex src.push("} else {"); src.push(" vec4 worldPosition = positionsDecodeMatrix * vec4(position, 1.0); "); src.push(" worldPosition = worldMatrix * vec4(dot(worldPosition, modelMatrixCol0), dot(worldPosition, modelMatrixCol1), dot(worldPosition, modelMatrixCol2), 1.0);"); if (scene.entityOffsetsEnabled) { src.push(" worldPosition.xyz = worldPosition.xyz + offset;"); } src.push(" vec4 viewPosition = viewMatrix * worldPosition; "); if (clipping) { src.push(" vWorldPosition = worldPosition;"); src.push(" vFlags = flags;"); } src.push(" vViewPosition = viewPosition;"); src.push("vec4 clipPos = projMatrix * viewPosition;"); src.push("gl_Position = remapClipPos(clipPos);"); if (scene.logarithmicDepthBufferEnabled) { src.push("vFragDepth = 1.0 + clipPos.w;"); } src.push("gl_Position = remapClipPos(clipPos);"); if (pointsMaterial.perspectivePoints) { src.push("gl_PointSize = (nearPlaneHeight * pointSize) / clipPos.w;"); src.push("gl_PointSize = max(gl_PointSize, " + Math.floor(pointsMaterial.minPerspectivePointSize) + ".0);"); src.push("gl_PointSize = min(gl_PointSize, " + Math.floor(pointsMaterial.maxPerspectivePointSize) + ".0);"); } else { src.push("gl_PointSize = pointSize;"); } src.push("}"); src.push("}"); return src; } _buildFragmentShader() { const scene = this._scene; const sectionPlanesState = scene._sectionPlanesState; const clipping = sectionPlanesState.getNumAllocatedSectionPlanes() > 0; const src = []; src.push('#version 300 es'); src.push("// Points instancing pick depth fragment shader"); src.push("#ifdef GL_FRAGMENT_PRECISION_HIGH"); src.push("precision highp float;"); src.push("precision highp int;"); src.push("#else"); src.push("precision mediump float;"); src.push("precision mediump int;"); src.push("#endif"); if (scene.logarithmicDepthBufferEnabled) { src.push("uniform float logDepthBufFC;"); src.push("in float vFragDepth;"); } src.push("uniform float pickZNear;"); src.push("uniform float pickZFar;"); if (clipping) { src.push("in vec4 vWorldPosition;"); src.push("in float vFlags;"); for (let i = 0; i < sectionPlanesState.getNumAllocatedSectionPlanes(); i++) { src.push("uniform bool sectionPlaneActive" + i + ";"); src.push("uniform vec3 sectionPlanePos" + i + ";"); src.push("uniform vec3 sectionPlaneDir" + i + ";"); } } src.push("in vec4 vViewPosition;"); src.push("vec4 packDepth(const in float depth) {"); src.push(" const vec4 bitShift = vec4(256.0*256.0*256.0, 256.0*256.0, 256.0, 1.0);"); src.push(" const vec4 bitMask = vec4(0.0, 1.0/256.0, 1.0/256.0, 1.0/256.0);"); src.push(" vec4 res = fract(depth * bitShift);"); src.push(" res -= res.xxyz * bitMask;"); src.push(" return res;"); src.push("}"); src.push("out vec4 outColor;"); src.push("void main(void) {"); if (scene.pointsMaterial.roundPoints) { src.push(" vec2 cxy = 2.0 * gl_PointCoord - 1.0;"); src.push(" float r = dot(cxy, cxy);"); src.push(" if (r > 1.0) {"); src.push(" discard;"); src.push(" }"); } if (clipping) { src.push(" bool clippable = (int(vFlags) >> 16 & 0xF) == 1;"); src.push(" if (clippable) {"); src.push(" float dist = 0.0;"); for (let i = 0; i < sectionPlanesState.getNumAllocatedSectionPlanes(); i++) { src.push("if (sectionPlaneActive" + i + ") {"); src.push(" dist += clamp(dot(-sectionPlaneDir" + i + ".xyz, vWorldPosition.xyz - sectionPlanePos" + i + ".xyz), 0.0, 1000.0);"); src.push("}"); } src.push("if (dist > 0.0) { discard; }"); src.push("}"); } if (scene.logarithmicDepthBufferEnabled) { src.push("gl_FragDepth = log2( vFragDepth ) * logDepthBufFC * 0.5;"); } src.push(" float zNormalizedDepth = abs((pickZNear + vViewPosition.z) / (pickZFar - pickZNear));"); src.push(" outColor = packDepth(zNormalizedDepth); "); // Must be linear depth src.push("}"); return src; } } /** * @private */ class VBOInstancingPointsOcclusionRenderer extends VBOInstancingPointsRenderer { _getHash() { return this._scene._sectionPlanesState.getHash() + this._scene.pointsMaterial.hash; } _buildVertexShader() { const scene = this._scene; const sectionPlanesState = scene._sectionPlanesState; const clipping = sectionPlanesState.getNumAllocatedSectionPlanes() > 0; const pointsMaterial = scene.pointsMaterial._state; const src = []; src.push ('#version 300 es'); src.push("// Points instancing occlusion vertex shader"); src.push("uniform int renderPass;"); src.push("in vec3 position;"); if (scene.entityOffsetsEnabled) { src.push("in vec3 offset;"); } src.push("in vec4 color;"); src.push("in float flags;"); src.push("in vec4 modelMatrixCol0;"); // Modeling matrix src.push("in vec4 modelMatrixCol1;"); src.push("in vec4 modelMatrixCol2;"); this._addMatricesUniformBlockLines(src); src.push("uniform float pointSize;"); if (pointsMaterial.perspectivePoints) { src.push("uniform float nearPlaneHeight;"); } if (scene.logarithmicDepthBufferEnabled) { src.push("uniform float logDepthBufFC;"); src.push("out float vFragDepth;"); } if (clipping) { src.push("out vec4 vWorldPosition;"); src.push("out float vFlags;"); } src.push("void main(void) {"); // colorFlag = NOT_RENDERED | COLOR_OPAQUE | COLOR_TRANSPARENT // renderPass = COLOR_OPAQUE src.push(`int colorFlag = int(flags) & 0xF;`); src.push(`if (colorFlag != renderPass) {`); src.push(" gl_Position = vec4(0.0, 0.0, 0.0, 0.0);"); src.push("} else {"); src.push(" vec4 worldPosition = positionsDecodeMatrix * vec4(position, 1.0); "); src.push(" worldPosition = worldMatrix * vec4(dot(worldPosition, modelMatrixCol0), dot(worldPosition, modelMatrixCol1), dot(worldPosition, modelMatrixCol2), 1.0);"); if (scene.entityOffsetsEnabled) { src.push(" worldPosition.xyz = worldPosition.xyz + offset;"); } src.push(" vec4 viewPosition = viewMatrix * worldPosition; "); if (clipping) { src.push(" vWorldPosition = worldPosition;"); src.push("vFlags = flags;"); } src.push("vec4 clipPos = projMatrix * viewPosition;"); if (scene.logarithmicDepthBufferEnabled) { src.push("vFragDepth = 1.0 + clipPos.w;"); } src.push("gl_Position = clipPos;"); if (pointsMaterial.perspectivePoints) { src.push("gl_PointSize = (nearPlaneHeight * pointSize) / clipPos.w;"); src.push("gl_PointSize = max(gl_PointSize, " + Math.floor(pointsMaterial.minPerspectivePointSize) + ".0);"); src.push("gl_PointSize = min(gl_PointSize, " + Math.floor(pointsMaterial.maxPerspectivePointSize) + ".0);"); } else { src.push("gl_PointSize = pointSize;"); } src.push("}"); src.push("}"); return src; } _buildFragmentShader() { const scene = this._scene; const sectionPlanesState = scene._sectionPlanesState; const clipping = sectionPlanesState.getNumAllocatedSectionPlanes() > 0; const src = []; src.push ('#version 300 es'); src.push("// Points instancing occlusion vertex shader"); src.push("#ifdef GL_FRAGMENT_PRECISION_HIGH"); src.push("precision highp float;"); src.push("precision highp int;"); src.push("#else"); src.push("precision mediump float;"); src.push("precision mediump int;"); src.push("#endif"); if (scene.logarithmicDepthBufferEnabled) { src.push("uniform float logDepthBufFC;"); src.push("in float vFragDepth;"); } if (clipping) { src.push("in vec4 vWorldPosition;"); src.push("in float vFlags;"); for (let i = 0; i < sectionPlanesState.getNumAllocatedSectionPlanes(); i++) { src.push("uniform bool sectionPlaneActive" + i + ";"); src.push("uniform vec3 sectionPlanePos" + i + ";"); src.push("uniform vec3 sectionPlaneDir" + i + ";"); } } src.push("out vec4 outColor;"); src.push("void main(void) {"); if (scene.pointsMaterial.roundPoints) { src.push(" vec2 cxy = 2.0 * gl_PointCoord - 1.0;"); src.push(" float r = dot(cxy, cxy);"); src.push(" if (r > 1.0) {"); src.push(" discard;"); src.push(" }"); } if (clipping) { src.push(" bool clippable = (int(vFlags) >> 16 & 0xF) == 1;"); src.push(" if (clippable) {"); src.push(" float dist = 0.0;"); for (let i = 0; i < sectionPlanesState.getNumAllocatedSectionPlanes(); i++) { src.push("if (sectionPlaneActive" + i + ") {"); src.push(" dist += clamp(dot(-sectionPlaneDir" + i + ".xyz, vWorldPosition.xyz - sectionPlanePos" + i + ".xyz), 0.0, 1000.0);"); src.push("}"); } src.push("if (dist > 0.0) { discard; }"); src.push("}"); } src.push(" outColor = vec4(0.0, 0.0, 1.0, 1.0); "); // Occluders are blue if (scene.logarithmicDepthBufferEnabled) { src.push("gl_FragDepth = log2( vFragDepth ) * logDepthBufFC * 0.5;"); } src.push("}"); return src; } } /** * @private */ class VBOInstancingPointsDepthRenderer extends VBOInstancingPointsRenderer { _getHash() { return this._scene._sectionPlanesState.getHash() + this._scene.pointsMaterial.hash; } _buildVertexShader() { const scene = this._scene; const sectionPlanesState = scene._sectionPlanesState; const clipping = sectionPlanesState.getNumAllocatedSectionPlanes() > 0; const pointsMaterial = scene.pointsMaterial._state; const src = []; src.push('#version 300 es'); src.push("// Points instancing depth vertex shader"); src.push("uniform int renderPass;"); src.push("in vec3 position;"); if (scene.entityOffsetsEnabled) { src.push("in vec3 offset;"); } src.push("in float flags;"); src.push("in vec4 modelMatrixCol0;"); src.push("in vec4 modelMatrixCol1;"); src.push("in vec4 modelMatrixCol2;"); this._addMatricesUniformBlockLines(src); src.push("uniform float pointSize;"); if (pointsMaterial.perspectivePoints) { src.push("uniform float nearPlaneHeight;"); } if (scene.logarithmicDepthBufferEnabled) { src.push("uniform float logDepthBufFC;"); src.push("out float vFragDepth;"); } if (clipping) { src.push("out vec4 vWorldPosition;"); src.push("out float vFlags;"); } src.push("void main(void) {"); // colorFlag = NOT_RENDERED | COLOR_OPAQUE | COLOR_TRANSPARENT // renderPass = COLOR_OPAQUE src.push(`int colorFlag = int(flags) & 0xF;`); src.push(`if (colorFlag != renderPass) {`); src.push(" gl_Position = vec4(0.0, 0.0, 0.0, 0.0);"); // Cull vertex src.push("} else {"); src.push(" vec4 worldPosition = positionsDecodeMatrix * vec4(position, 1.0); "); src.push(" worldPosition = worldMatrix * vec4(dot(worldPosition, modelMatrixCol0), dot(worldPosition, modelMatrixCol1), dot(worldPosition, modelMatrixCol2), 1.0);"); if (scene.entityOffsetsEnabled) { src.push(" worldPosition.xyz = worldPosition.xyz + offset;"); } src.push(" vec4 viewPosition = viewMatrix * worldPosition; "); if (clipping) { src.push("vWorldPosition = worldPosition;"); src.push("vFlags = flags;"); } src.push("vec4 clipPos = projMatrix * viewPosition;"); if (scene.logarithmicDepthBufferEnabled) { src.push("vFragDepth = 1.0 + clipPos.w;"); } src.push("gl_Position = clipPos;"); if (pointsMaterial.perspectivePoints) { src.push("gl_PointSize = (nearPlaneHeight * pointSize) / clipPos.w;"); src.push("gl_PointSize = max(gl_PointSize, " + Math.floor(pointsMaterial.minPerspectivePointSize) + ".0);"); src.push("gl_PointSize = min(gl_PointSize, " + Math.floor(pointsMaterial.maxPerspectivePointSize) + ".0);"); } else { src.push("gl_PointSize = pointSize;"); } src.push("}"); src.push("}"); return src; } _buildFragmentShader() { const scene = this._scene; const sectionPlanesState = scene._sectionPlanesState; let i; let len; const clipping = sectionPlanesState.getNumAllocatedSectionPlanes() > 0; const src = []; src.push('#version 300 es'); src.push("// Points instancing depth vertex shader"); src.push("#ifdef GL_FRAGMENT_PRECISION_HIGH"); src.push("precision highp float;"); src.push("precision highp int;"); src.push("#else"); src.push("precision mediump float;"); src.push("precision mediump int;"); src.push("#endif"); if (scene.logarithmicDepthBufferEnabled) { src.push("uniform float logDepthBufFC;"); src.push("in float vFragDepth;"); } if (clipping) { src.push("in vec4 vWorldPosition;"); src.push("in float vFlags;"); for (i = 0, len = sectionPlanesState.getNumAllocatedSectionPlanes(); i < len; i++) { src.push("uniform bool sectionPlaneActive" + i + ";"); src.push("uniform vec3 sectionPlanePos" + i + ";"); src.push("uniform vec3 sectionPlaneDir" + i + ";"); } } src.push("const float packUpScale = 256. / 255.;"); src.push("const float unpackDownscale = 255. / 256.;"); src.push("const vec3 packFactors = vec3( 256. * 256. * 256., 256. * 256., 256. );"); src.push("const vec4 unpackFactors = unpackDownscale / vec4( packFactors, 1. );"); src.push("const float shiftRight8 = 1.0 / 256.;"); src.push("vec4 packDepthToRGBA( const in float v ) {"); src.push(" vec4 r = vec4( fract( v * packFactors ), v );"); src.push(" r.yzw -= r.xyz * shiftRight8;"); src.push(" return r * packUpScale;"); src.push("}"); src.push("out vec4 outColor;"); src.push("void main(void) {"); if (scene.pointsMaterial.roundPoints) { src.push(" vec2 cxy = 2.0 * gl_PointCoord - 1.0;"); src.push(" float r = dot(cxy, cxy);"); src.push(" if (r > 1.0) {"); src.push(" discard;"); src.push(" }"); } if (clipping) { src.push(" bool clippable = (int(vFlags) >> 16 & 0xF) == 1;"); src.push(" if (clippable) {"); src.push(" float dist = 0.0;"); for (i = 0, len = sectionPlanesState.getNumAllocatedSectionPlanes(); i < len; i++) { src.push("if (sectionPlaneActive" + i + ") {"); src.push(" dist += clamp(dot(-sectionPlaneDir" + i + ".xyz, vWorldPosition.xyz - sectionPlanePos" + i + ".xyz), 0.0, 1000.0);"); src.push("}"); } src.push("if (dist > 0.0) { discard; }"); src.push("}"); } src.push(" outColor = packDepthToRGBA( gl_FragCoord.z); "); // Must be linear depth if (scene.logarithmicDepthBufferEnabled) { src.push("gl_FragDepth = log2( vFragDepth ) * logDepthBufFC * 0.5;"); } src.push("}"); return src; } } /** * Renders InstancingLayer fragment depths to a shadow map. * * @private */ class VBOInstancingPointsShadowRenderer extends VBOInstancingPointsRenderer { _buildVertexShader() { const scene = this._scene; const sectionPlanesState = scene._sectionPlanesState; const clipping = sectionPlanesState.getNumAllocatedSectionPlanes() > 0; const src = []; src.push ('#version 300 es'); src.push("// Instancing geometry shadow drawing vertex shader"); src.push("in vec3 position;"); if (scene.entityOffsetsEnabled) { src.push("in vec3 offset;"); } src.push("in vec4 color;"); src.push("in float flags;"); src.push("in vec4 modelMatrixCol0;"); src.push("in vec4 modelMatrixCol1;"); src.push("in vec4 modelMatrixCol2;"); src.push("uniform mat4 shadowViewMatrix;"); src.push("uniform mat4 shadowProjMatrix;"); this._addMatricesUniformBlockLines(src); src.push("uniform float pointSize;"); if (clipping) { src.push("out vec4 vWorldPosition;"); src.push("out float vFlags;"); } src.push("void main(void) {"); src.push("int colorFlag = int(flags) & 0xF;"); src.push("bool visible = (colorFlag > 0);"); src.push("bool transparent = ((float(color.a) / 255.0) < 1.0);"); src.push(`if (!visible || transparent) {`); src.push(" gl_Position = vec4(0.0, 0.0, 0.0, 0.0);"); // Cull vertex src.push("} else {"); src.push(" vec4 worldPosition = positionsDecodeMatrix * vec4(position, 1.0); "); src.push(" worldPosition = vec4(dot(worldPosition, modelMatrixCol0), dot(worldPosition, modelMatrixCol1), dot(worldPosition, modelMatrixCol2), 1.0);"); if (scene.entityOffsetsEnabled) { src.push(" worldPosition.xyz = worldPosition.xyz + offset;"); } src.push(" vec4 viewPosition = shadowViewMatrix * worldPosition; "); if (clipping) { src.push("vWorldPosition = worldPosition;"); src.push("vFlags = flags;"); } src.push(" gl_Position = shadowProjMatrix * viewPosition;"); src.push("}"); src.push("gl_PointSize = pointSize;"); src.push("}"); return src; } _buildFragmentShader() { const scene = this._scene; const sectionPlanesState = scene._sectionPlanesState; const clipping = sectionPlanesState.getNumAllocatedSectionPlanes() > 0; const src = []; src.push ('#version 300 es'); src.push("// Instancing geometry depth drawing fragment shader"); src.push("#ifdef GL_FRAGMENT_PRECISION_HIGH"); src.push("precision highp float;"); src.push("precision highp int;"); src.push("#else"); src.push("precision mediump float;"); src.push("precision mediump int;"); src.push("#endif"); if (scene.logarithmicDepthBufferEnabled) { src.push("uniform float logDepthBufFC;"); src.push("in float vFragDepth;"); } if (clipping) { src.push("in vec4 vWorldPosition;"); src.push("in float vFlags;"); for (let i = 0, len = sectionPlanesState.getNumAllocatedSectionPlanes(); i < len; i++) { src.push("uniform bool sectionPlaneActive" + i + ";"); src.push("uniform vec3 sectionPlanePos" + i + ";"); src.push("uniform vec3 sectionPlaneDir" + i + ";"); } } src.push("in vec3 vViewNormal;"); src.push("vec3 packNormalToRGB( const in vec3 normal ) {"); src.push(" return normalize( normal ) * 0.5 + 0.5;"); src.push("}"); src.push("out vec4 outColor;"); src.push("void main(void) {"); src.push(" vec2 cxy = 2.0 * gl_PointCoord - 1.0;"); src.push(" float r = dot(cxy, cxy);"); src.push(" if (r > 1.0) {"); src.push(" discard;"); src.push(" }"); if (clipping) { src.push(" bool clippable = (int(vFlags) >> 16 & 0xF) == 1;"); src.push(" if (clippable) {"); src.push(" float dist = 0.0;"); for (let i = 0, len = sectionPlanesState.getNumAllocatedSectionPlanes(); i < len; i++) { src.push("if (sectionPlaneActive" + i + ") {"); src.push(" dist += clamp(dot(-sectionPlaneDir" + i + ".xyz, vWorldPosition.xyz - sectionPlanePos" + i + ".xyz), 0.0, 1000.0);"); src.push("}"); } src.push("if (dist > 0.0) { discard; }"); src.push("}"); } if (scene.logarithmicDepthBufferEnabled) { src.push("gl_FragDepth = log2( vFragDepth ) * logDepthBufFC * 0.5;"); } src.push(" outColor = vec4(packNormalToRGB(vViewNormal), 1.0); "); src.push("}"); return src; } } const tempVec3a$s = math.vec3(); const tempVec3b$o = math.vec3(); const tempVec3c$j = math.vec3(); math.vec3(); const tempMat4a$g = math.mat4(); /** * @private */ class VBOInstancingPointsSnapInitRenderer extends VBORenderer { constructor(scene) { super(scene, false, { instancing: true }); } drawLayer(frameCtx, instancingLayer, renderPass) { if (!this._program) { this._allocate(); if (this.errors) { return; } } if (frameCtx.lastProgramId !== this._program.id) { frameCtx.lastProgramId = this._program.id; this._bindProgram(); } const model = instancingLayer.model; const scene = model.scene; const gl = scene.canvas.gl; const camera = scene.camera; const state = instancingLayer._state; const origin = instancingLayer._state.origin; const {position, rotationMatrix} = model; const aabb = instancingLayer.aabb; // Per-layer AABB for best RTC accuracy const viewMatrix = frameCtx.pickViewMatrix || camera.viewMatrix; if (this._vaoCache.has(instancingLayer)) { gl.bindVertexArray(this._vaoCache.get(instancingLayer)); } else { this._vaoCache.set(instancingLayer, this._makeVAO(state)); } const coordinateScaler = tempVec3a$s; coordinateScaler[0] = math.safeInv(aabb[3] - aabb[0]) * math.MAX_INT; coordinateScaler[1] = math.safeInv(aabb[4] - aabb[1]) * math.MAX_INT; coordinateScaler[2] = math.safeInv(aabb[5] - aabb[2]) * math.MAX_INT; frameCtx.snapPickCoordinateScale[0] = math.safeInv(coordinateScaler[0]); frameCtx.snapPickCoordinateScale[1] = math.safeInv(coordinateScaler[1]); frameCtx.snapPickCoordinateScale[2] = math.safeInv(coordinateScaler[2]); let rtcViewMatrix; if (origin || position[0] !== 0 || position[1] !== 0 || position[2] !== 0) { const rtcOrigin = tempVec3b$o; if (origin) { const rotatedOrigin = math.transformPoint3(rotationMatrix, origin, tempVec3c$j); rtcOrigin[0] = rotatedOrigin[0]; rtcOrigin[1] = rotatedOrigin[1]; rtcOrigin[2] = rotatedOrigin[2]; } else { rtcOrigin[0] = 0; rtcOrigin[1] = 0; rtcOrigin[2] = 0; } rtcOrigin[0] += position[0]; rtcOrigin[1] += position[1]; rtcOrigin[2] += position[2]; rtcViewMatrix = createRTCViewMat(viewMatrix, rtcOrigin, tempMat4a$g); frameCtx.snapPickOrigin[0] = rtcOrigin[0]; frameCtx.snapPickOrigin[1] = rtcOrigin[1]; frameCtx.snapPickOrigin[2] = rtcOrigin[2]; } else { rtcViewMatrix = viewMatrix; frameCtx.snapPickOrigin[0] = 0; frameCtx.snapPickOrigin[1] = 0; frameCtx.snapPickOrigin[2] = 0; } gl.uniform2fv(this.uVectorA, frameCtx.snapVectorA); gl.uniform2fv(this.uInverseVectorAB, frameCtx.snapInvVectorAB); gl.uniform1i(this._uLayerNumber, frameCtx.snapPickLayerNumber); gl.uniform3fv(this._uCoordinateScaler, coordinateScaler); gl.uniform1i(this._uRenderPass, renderPass); gl.uniform1i(this._uPickInvisible, frameCtx.pickInvisible); let offset = 0; const mat4Size = 4 * 4; this._matricesUniformBlockBufferData.set(rotationMatrix, 0); this._matricesUniformBlockBufferData.set(rtcViewMatrix, offset += mat4Size); this._matricesUniformBlockBufferData.set(camera.projMatrix, offset += mat4Size); this._matricesUniformBlockBufferData.set(state.positionsDecodeMatrix, offset += mat4Size); gl.bindBuffer(gl.UNIFORM_BUFFER, this._matricesUniformBlockBuffer); gl.bufferData(gl.UNIFORM_BUFFER, this._matricesUniformBlockBufferData, gl.DYNAMIC_DRAW); gl.bindBufferBase( gl.UNIFORM_BUFFER, this._matricesUniformBlockBufferBindingPoint, this._matricesUniformBlockBuffer); { const logDepthBufFC = 2.0 / (Math.log(frameCtx.pickZFar + 1.0) / Math.LN2); gl.uniform1f(this._uLogDepthBufFC, logDepthBufFC); } this.setSectionPlanesStateUniforms(instancingLayer); gl.drawArraysInstanced(gl.POINTS, 0, state.positionsBuf.numItems, state.numInstances); gl.bindVertexArray(null); } _allocate() { super._allocate(); const program = this._program; { this._uLogDepthBufFC = program.getLocation("logDepthBufFC"); } this.uVectorA = program.getLocation("snapVectorA"); this.uInverseVectorAB = program.getLocation("snapInvVectorAB"); this._uLayerNumber = program.getLocation("layerNumber"); this._uCoordinateScaler = program.getLocation("coordinateScaler"); } _bindProgram() { this._program.bind(); } _buildVertexShader() { const scene = this._scene; const sectionPlanesState = scene._sectionPlanesState; const clipping = sectionPlanesState.getNumAllocatedSectionPlanes() > 0; const src = []; src.push ('#version 300 es'); src.push("// SnapInstancingDepthBufInitRenderer vertex shader"); src.push("#ifdef GL_FRAGMENT_PRECISION_HIGH"); src.push("precision highp float;"); src.push("precision highp int;"); src.push("precision highp usampler2D;"); src.push("precision highp isampler2D;"); src.push("precision highp sampler2D;"); src.push("#else"); src.push("precision mediump float;"); src.push("precision mediump int;"); src.push("precision mediump usampler2D;"); src.push("precision mediump isampler2D;"); src.push("precision mediump sampler2D;"); src.push("#endif"); src.push("uniform int renderPass;"); src.push("in vec4 pickColor;"); src.push("in vec3 position;"); if (scene.entityOffsetsEnabled) { src.push("in vec3 offset;"); } src.push("in float flags;"); src.push("in vec4 modelMatrixCol0;"); // Modeling matrix src.push("in vec4 modelMatrixCol1;"); src.push("in vec4 modelMatrixCol2;"); src.push("uniform bool pickInvisible;"); this._addMatricesUniformBlockLines(src); src.push("uniform vec2 snapVectorA;"); src.push("uniform vec2 snapInvVectorAB;"); { src.push("uniform float logDepthBufFC;"); src.push("out float vFragDepth;"); src.push("bool isPerspectiveMatrix(mat4 m) {"); src.push(" return (m[2][3] == - 1.0);"); src.push("}"); src.push("out float isPerspective;"); } src.push("vec2 remapClipPos(vec2 clipPos) {"); src.push(" float x = (clipPos.x - snapVectorA.x) * snapInvVectorAB.x;"); src.push(" float y = (clipPos.y - snapVectorA.y) * snapInvVectorAB.y;"); src.push(" return vec2(x, y);"); src.push("}"); src.push("flat out vec4 vPickColor;"); src.push("out vec4 vWorldPosition;"); if (clipping) { src.push("out float vFlags;"); } src.push("out highp vec3 relativeToOriginPosition;"); src.push("void main(void) {"); // pickFlag = NOT_RENDERED | PICK // renderPass = PICK src.push(`int pickFlag = int(flags) >> 12 & 0xF;`); src.push(`if (pickFlag != renderPass) {`); src.push(" gl_Position = vec4(0.0, 0.0, 0.0, 0.0);"); // Cull vertex src.push("} else {"); src.push(" vec4 worldPosition = positionsDecodeMatrix * vec4(position, 1.0); "); src.push(" worldPosition = worldMatrix * vec4(dot(worldPosition, modelMatrixCol0), dot(worldPosition, modelMatrixCol1), dot(worldPosition, modelMatrixCol2), 1.0);"); if (scene.entityOffsetsEnabled) { src.push(" worldPosition.xyz = worldPosition.xyz + offset;"); } src.push("relativeToOriginPosition = worldPosition.xyz;"); src.push(" vec4 viewPosition = viewMatrix * worldPosition; "); src.push(" vWorldPosition = worldPosition;"); if (clipping) { src.push(" vFlags = flags;"); } src.push("vPickColor = pickColor;"); src.push("vec4 clipPos = projMatrix * viewPosition;"); src.push("float tmp = clipPos.w;"); src.push("clipPos.xyzw /= tmp;"); src.push("clipPos.xy = remapClipPos(clipPos.xy);"); src.push("clipPos.xyzw *= tmp;"); { src.push("vFragDepth = 1.0 + clipPos.w;"); src.push("isPerspective = float (isPerspectiveMatrix(projMatrix));"); } src.push("gl_Position = clipPos;"); src.push("}"); src.push("}"); return src; } _buildFragmentShader() { const scene = this._scene; const sectionPlanesState = scene._sectionPlanesState; const clipping = sectionPlanesState.getNumAllocatedSectionPlanes() > 0; const src = []; src.push ('#version 300 es'); src.push("// Points instancing pick depth fragment shader"); src.push("#ifdef GL_FRAGMENT_PRECISION_HIGH"); src.push("precision highp float;"); src.push("precision highp int;"); src.push("#else"); src.push("precision mediump float;"); src.push("precision mediump int;"); src.push("#endif"); { src.push("in float isPerspective;"); src.push("uniform float logDepthBufFC;"); src.push("in float vFragDepth;"); } src.push("uniform int layerNumber;"); src.push("uniform vec3 coordinateScaler;"); src.push("in vec4 vWorldPosition;"); src.push("flat in vec4 vPickColor;"); if (clipping) { src.push("in float vFlags;"); for (let i = 0; i < sectionPlanesState.getNumAllocatedSectionPlanes(); i++) { src.push("uniform bool sectionPlaneActive" + i + ";"); src.push("uniform vec3 sectionPlanePos" + i + ";"); src.push("uniform vec3 sectionPlaneDir" + i + ";"); } } src.push("in highp vec3 relativeToOriginPosition;"); src.push("layout(location = 0) out highp ivec4 outCoords;"); src.push("layout(location = 1) out highp ivec4 outNormal;"); src.push("layout(location = 2) out lowp uvec4 outPickColor;"); src.push("void main(void) {"); if (clipping) { src.push(" bool clippable = (int(vFlags) >> 16 & 0xF) == 1;"); src.push(" if (clippable) {"); src.push(" float dist = 0.0;"); for (let i = 0; i < sectionPlanesState.getNumAllocatedSectionPlanes(); i++) { src.push("if (sectionPlaneActive" + i + ") {"); src.push(" dist += clamp(dot(-sectionPlaneDir" + i + ".xyz, vWorldPosition.xyz - sectionPlanePos" + i + ".xyz), 0.0, 1000.0);"); src.push("}"); } src.push("if (dist > 0.0) { discard; }"); src.push("}"); } { src.push(" float dx = dFdx(vFragDepth);"); src.push(" float dy = dFdy(vFragDepth);"); src.push(" float diff = sqrt(dx*dx+dy*dy);"); src.push(" gl_FragDepth = isPerspective == 0.0 ? gl_FragCoord.z : log2( vFragDepth + diff ) * logDepthBufFC * 0.5;"); } src.push("outCoords = ivec4(relativeToOriginPosition.xyz*coordinateScaler.xyz, -layerNumber);"); // src.push("vec3 xTangent = dFdx( vWorldPosition.xyz );"); // src.push("vec3 yTangent = dFdy( vWorldPosition.xyz );"); // src.push("vec3 worldNormal = normalize( cross( xTangent, yTangent ) );"); src.push(`outNormal = ivec4(1.0, 1.0, 1.0, 1.0);`); src.push("outPickColor = uvec4(vPickColor);"); src.push("}"); return src; } webglContextRestored() { this._program = null; } destroy() { if (this._program) { this._program.destroy(); } this._program = null; } } const tempVec3a$r = math.vec3(); const tempVec3b$n = math.vec3(); const tempVec3c$i = math.vec3(); math.vec3(); const tempMat4a$f = math.mat4(); /** * @private */ class VBOInstancingPointsSnapRenderer extends VBORenderer { constructor(scene) { super(scene, false, { instancing: true }); } drawLayer(frameCtx, instancingLayer, renderPass) { if (!this._program) { this._allocate(instancingLayer); if (this.errors) { return; } } if (frameCtx.lastProgramId !== this._program.id) { frameCtx.lastProgramId = this._program.id; this._bindProgram(); } const model = instancingLayer.model; const scene = model.scene; const camera = scene.camera; const gl = scene.canvas.gl; const state = instancingLayer._state; const origin = instancingLayer._state.origin; const {position, rotationMatrix} = model; const aabb = instancingLayer.aabb; // Per-layer AABB for best RTC accuracy const viewMatrix = frameCtx.pickViewMatrix || camera.viewMatrix; if (this._vaoCache.has(instancingLayer)) { gl.bindVertexArray(this._vaoCache.get(instancingLayer)); } else { this._vaoCache.set(instancingLayer, this._makeVAO(state)); } const coordinateScaler = tempVec3a$r; coordinateScaler[0] = math.safeInv(aabb[3] - aabb[0]) * math.MAX_INT; coordinateScaler[1] = math.safeInv(aabb[4] - aabb[1]) * math.MAX_INT; coordinateScaler[2] = math.safeInv(aabb[5] - aabb[2]) * math.MAX_INT; frameCtx.snapPickCoordinateScale[0] = math.safeInv(coordinateScaler[0]); frameCtx.snapPickCoordinateScale[1] = math.safeInv(coordinateScaler[1]); frameCtx.snapPickCoordinateScale[2] = math.safeInv(coordinateScaler[2]); let rtcViewMatrix; if (origin || position[0] !== 0 || position[1] !== 0 || position[2] !== 0) { const rtcOrigin = tempVec3b$n; if (origin) { const rotatedOrigin = math.transformPoint3(rotationMatrix, origin, tempVec3c$i); rtcOrigin[0] = rotatedOrigin[0]; rtcOrigin[1] = rotatedOrigin[1]; rtcOrigin[2] = rotatedOrigin[2]; } else { rtcOrigin[0] = 0; rtcOrigin[1] = 0; rtcOrigin[2] = 0; } rtcOrigin[0] += position[0]; rtcOrigin[1] += position[1]; rtcOrigin[2] += position[2]; rtcViewMatrix = createRTCViewMat(viewMatrix, rtcOrigin, tempMat4a$f); frameCtx.snapPickOrigin[0] = rtcOrigin[0]; frameCtx.snapPickOrigin[1] = rtcOrigin[1]; frameCtx.snapPickOrigin[2] = rtcOrigin[2]; } else { rtcViewMatrix = viewMatrix; frameCtx.snapPickOrigin[0] = 0; frameCtx.snapPickOrigin[1] = 0; frameCtx.snapPickOrigin[2] = 0; } gl.uniform2fv(this.uVectorA, frameCtx.snapVectorA); gl.uniform2fv(this.uInverseVectorAB, frameCtx.snapInvVectorAB); gl.uniform1i(this._uLayerNumber, frameCtx.snapPickLayerNumber); gl.uniform3fv(this._uCoordinateScaler, coordinateScaler); gl.uniform1i(this._uRenderPass, renderPass); gl.uniform1i(this._uPickInvisible, frameCtx.pickInvisible); let offset = 0; const mat4Size = 4 * 4; this._matricesUniformBlockBufferData.set(rotationMatrix, 0); this._matricesUniformBlockBufferData.set(rtcViewMatrix, offset += mat4Size); this._matricesUniformBlockBufferData.set(camera.projMatrix, offset += mat4Size); this._matricesUniformBlockBufferData.set(state.positionsDecodeMatrix, offset += mat4Size); gl.bindBuffer(gl.UNIFORM_BUFFER, this._matricesUniformBlockBuffer); gl.bufferData(gl.UNIFORM_BUFFER, this._matricesUniformBlockBufferData, gl.DYNAMIC_DRAW); gl.bindBufferBase( gl.UNIFORM_BUFFER, this._matricesUniformBlockBufferBindingPoint, this._matricesUniformBlockBuffer); { const logDepthBufFC = 2.0 / (Math.log(frameCtx.pickZFar + 1.0) / Math.LN2); // TODO: Far from pick project matrix gl.uniform1f(this._uLogDepthBufFC, logDepthBufFC); } this.setSectionPlanesStateUniforms(instancingLayer); gl.drawArraysInstanced(gl.POINTS, 0, state.positionsBuf.numItems, state.numInstances); gl.bindVertexArray(null); } _allocate() { super._allocate(); const program = this._program; { this._uLogDepthBufFC = program.getLocation("logDepthBufFC"); } this.uVectorA = program.getLocation("snapVectorA"); this.uInverseVectorAB = program.getLocation("snapInvVectorAB"); this._uLayerNumber = program.getLocation("layerNumber"); this._uCoordinateScaler = program.getLocation("coordinateScaler"); } _bindProgram() { this._program.bind(); } _buildVertexShader() { const scene = this._scene; const sectionPlanesState = scene._sectionPlanesState; const clipping = sectionPlanesState.getNumAllocatedSectionPlanes() > 0; const src = []; src.push ('#version 300 es'); src.push("// SnapInstancingDepthRenderer vertex shader"); src.push("#ifdef GL_FRAGMENT_PRECISION_HIGH"); src.push("precision highp float;"); src.push("precision highp int;"); src.push("precision highp usampler2D;"); src.push("precision highp isampler2D;"); src.push("precision highp sampler2D;"); src.push("#else"); src.push("precision mediump float;"); src.push("precision mediump int;"); src.push("precision mediump usampler2D;"); src.push("precision mediump isampler2D;"); src.push("precision mediump sampler2D;"); src.push("#endif"); src.push("uniform int renderPass;"); src.push("in vec3 position;"); if (scene.entityOffsetsEnabled) { src.push("in vec3 offset;"); } src.push("in float flags;"); src.push("in vec4 modelMatrixCol0;"); // Modeling matrix src.push("in vec4 modelMatrixCol1;"); src.push("in vec4 modelMatrixCol2;"); src.push("uniform bool pickInvisible;"); this._addMatricesUniformBlockLines(src); src.push("uniform vec2 snapVectorA;"); src.push("uniform vec2 snapInvVectorAB;"); { src.push("uniform float logDepthBufFC;"); src.push("out float vFragDepth;"); src.push("bool isPerspectiveMatrix(mat4 m) {"); src.push(" return (m[2][3] == - 1.0);"); src.push("}"); src.push("out float isPerspective;"); } src.push("vec2 remapClipPos(vec2 clipPos) {"); src.push(" float x = (clipPos.x - snapVectorA.x) * snapInvVectorAB.x;"); src.push(" float y = (clipPos.y - snapVectorA.y) * snapInvVectorAB.y;"); src.push(" return vec2(x, y);"); src.push("}"); if (clipping) { src.push("out vec4 vWorldPosition;"); src.push("out float vFlags;"); } src.push("out highp vec3 relativeToOriginPosition;"); src.push("void main(void) {"); // pickFlag = NOT_RENDERED | PICK // renderPass = PICK src.push(`int pickFlag = int(flags) >> 12 & 0xF;`); src.push(`if (pickFlag != renderPass) {`); src.push(" gl_Position = vec4(0.0, 0.0, 0.0, 0.0);"); // Cull vertex src.push("} else {"); src.push(" vec4 worldPosition = positionsDecodeMatrix * vec4(position, 1.0); "); src.push(" worldPosition = worldMatrix * vec4(dot(worldPosition, modelMatrixCol0), dot(worldPosition, modelMatrixCol1), dot(worldPosition, modelMatrixCol2), 1.0);"); if (scene.entityOffsetsEnabled) { src.push(" worldPosition.xyz = worldPosition.xyz + offset;"); } src.push("relativeToOriginPosition = worldPosition.xyz;"); src.push(" vec4 viewPosition = viewMatrix * worldPosition; "); if (clipping) { src.push(" vWorldPosition = worldPosition;"); src.push(" vFlags = flags;"); } src.push("vec4 clipPos = projMatrix * viewPosition;"); src.push("float tmp = clipPos.w;"); src.push("clipPos.xyzw /= tmp;"); src.push("clipPos.xy = remapClipPos(clipPos.xy);"); src.push("clipPos.xyzw *= tmp;"); { src.push("vFragDepth = 1.0 + clipPos.w;"); src.push("isPerspective = float (isPerspectiveMatrix(projMatrix));"); } src.push("gl_Position = clipPos;"); src.push("gl_PointSize = 1.0;"); // Windows needs this? src.push("}"); src.push("}"); return src; } _buildFragmentShader() { const scene = this._scene; const sectionPlanesState = scene._sectionPlanesState; const clipping = sectionPlanesState.getNumAllocatedSectionPlanes() > 0; const src = []; src.push ('#version 300 es'); src.push("// SnapInstancingDepthRenderer fragment shader"); src.push("#ifdef GL_FRAGMENT_PRECISION_HIGH"); src.push("precision highp float;"); src.push("precision highp int;"); src.push("#else"); src.push("precision mediump float;"); src.push("precision mediump int;"); src.push("#endif"); { src.push("in float isPerspective;"); src.push("uniform float logDepthBufFC;"); src.push("in float vFragDepth;"); } src.push("uniform int layerNumber;"); src.push("uniform vec3 coordinateScaler;"); if (clipping) { src.push("in vec4 vWorldPosition;"); src.push("in float vFlags;"); for (let i = 0; i < sectionPlanesState.getNumAllocatedSectionPlanes(); i++) { src.push("uniform bool sectionPlaneActive" + i + ";"); src.push("uniform vec3 sectionPlanePos" + i + ";"); src.push("uniform vec3 sectionPlaneDir" + i + ";"); } } src.push("in highp vec3 relativeToOriginPosition;"); src.push("out highp ivec4 outCoords;"); src.push("void main(void) {"); if (clipping) { src.push(" bool clippable = (int(vFlags) >> 16 & 0xF) == 1;"); src.push(" if (clippable) {"); src.push(" float dist = 0.0;"); for (let i = 0; i < sectionPlanesState.getNumAllocatedSectionPlanes(); i++) { src.push("if (sectionPlaneActive" + i + ") {"); src.push(" dist += clamp(dot(-sectionPlaneDir" + i + ".xyz, vWorldPosition.xyz - sectionPlanePos" + i + ".xyz), 0.0, 1000.0);"); src.push("}"); } src.push("if (dist > 0.0) { discard; }"); src.push("}"); } { src.push(" gl_FragDepth = isPerspective == 0.0 ? gl_FragCoord.z : log2( vFragDepth ) * logDepthBufFC * 0.5;"); } src.push("outCoords = ivec4(relativeToOriginPosition.xyz*coordinateScaler.xyz, layerNumber);"); src.push("}"); return src; } webglContextRestored() { this._program = null; } destroy() { if (this._program) { this._program.destroy(); } this._program = null; } } /** * @private */ class VBOInstancingPointsRenderers { constructor(scene) { this._scene = scene; } _compile() { if (this._colorRenderer && (!this._colorRenderer.getValid())) { this._colorRenderer.destroy(); this._colorRenderer = null; } if (this._depthRenderer && (!this._depthRenderer.getValid())) { this._depthRenderer.destroy(); this._depthRenderer = null; } if (this._silhouetteRenderer && (!this._silhouetteRenderer.getValid())) { this._silhouetteRenderer.destroy(); this._silhouetteRenderer = null; } if (this._pickMeshRenderer && (!this._pickMeshRenderer.getValid())) { this._pickMeshRenderer.destroy(); this._pickMeshRenderer = null; } if (this._pickDepthRenderer && (!this._pickDepthRenderer.getValid())) { this._pickDepthRenderer.destroy(); this._pickDepthRenderer = null; } if (this._occlusionRenderer && this._occlusionRenderer.getValid() === false) { this._occlusionRenderer.destroy(); this._occlusionRenderer = null; } if (this._shadowRenderer && (!this._shadowRenderer.getValid())) { this._shadowRenderer.destroy(); this._shadowRenderer = null; } if (this._snapInitRenderer && (!this._snapInitRenderer.getValid())) { this._snapInitRenderer.destroy(); this._snapInitRenderer = null; } if (this._snapRenderer && (!this._snapRenderer.getValid())) { this._snapRenderer.destroy(); this._snapRenderer = null; } } get colorRenderer() { if (!this._colorRenderer) { this._colorRenderer = new VBOInstancingPointsColorRenderer(this._scene, false); } return this._colorRenderer; } get silhouetteRenderer() { if (!this._silhouetteRenderer) { this._silhouetteRenderer = new VBOInstancingPointsSilhouetteRenderer(this._scene); } return this._silhouetteRenderer; } get depthRenderer() { if (!this._depthRenderer) { this._depthRenderer = new VBOInstancingPointsDepthRenderer(this._scene); } return this._depthRenderer; } get pickMeshRenderer() { if (!this._pickMeshRenderer) { this._pickMeshRenderer = new VBOInstancingPointsPickMeshRenderer(this._scene); } return this._pickMeshRenderer; } get pickDepthRenderer() { if (!this._pickDepthRenderer) { this._pickDepthRenderer = new VBOInstancingPointsPickDepthRenderer(this._scene); } return this._pickDepthRenderer; } get occlusionRenderer() { if (!this._occlusionRenderer) { this._occlusionRenderer = new VBOInstancingPointsOcclusionRenderer(this._scene); } return this._occlusionRenderer; } get shadowRenderer() { if (!this._shadowRenderer) { this._shadowRenderer = new VBOInstancingPointsShadowRenderer(this._scene); } return this._shadowRenderer; } get snapInitRenderer() { if (!this._snapInitRenderer) { this._snapInitRenderer = new VBOInstancingPointsSnapInitRenderer(this._scene, false); } return this._snapInitRenderer; } get snapRenderer() { if (!this._snapRenderer) { this._snapRenderer = new VBOInstancingPointsSnapRenderer(this._scene); } return this._snapRenderer; } _destroy() { if (this._colorRenderer) { this._colorRenderer.destroy(); } if (this._depthRenderer) { this._depthRenderer.destroy(); } if (this._silhouetteRenderer) { this._silhouetteRenderer.destroy(); } if (this._pickMeshRenderer) { this._pickMeshRenderer.destroy(); } if (this._pickDepthRenderer) { this._pickDepthRenderer.destroy(); } if (this._occlusionRenderer) { this._occlusionRenderer.destroy(); } if (this._shadowRenderer) { this._shadowRenderer.destroy(); } if (this._snapInitRenderer) { this._snapInitRenderer.destroy(); } if (this._snapRenderer) { this._snapRenderer.destroy(); } } } const cachedRenderers$1 = {}; /** * @private */ function getRenderers$2(scene) { const sceneId = scene.id; let instancingRenderers = cachedRenderers$1[sceneId]; if (!instancingRenderers) { instancingRenderers = new VBOInstancingPointsRenderers(scene); cachedRenderers$1[sceneId] = instancingRenderers; instancingRenderers._compile(); scene.on("compile", () => { instancingRenderers._compile(); }); scene.on("destroyed", () => { delete cachedRenderers$1[sceneId]; instancingRenderers._destroy(); }); } return instancingRenderers; } const tempUint8Vec4 = new Uint8Array(4); const tempFloat32 = new Float32Array(1); const tempVec3fa = new Float32Array(3); const tempFloat32Vec4 = new Float32Array(4); /** * @private */ class VBOInstancingPointsLayer { /** * @param cfg * @param cfg.layerIndex * @param cfg.model * @param cfg.geometry * @param cfg.material * @param cfg.origin */ constructor(cfg) { // console.info("VBOInstancingPointsLayer"); /** * Owner model * @type {VBOSceneModel} */ this.model = cfg.model; /** * Shared material * @type {VBOSceneModelGeometry} */ this.material = cfg.material; /** * State sorting key. * @type {string} */ this.sortId = "PointsInstancingLayer"; /** * Index of this InstancingLayer in VBOSceneModel#_layerList * @type {Number} */ this.layerIndex = cfg.layerIndex; this._renderers = getRenderers$2(cfg.model.scene); this._aabb = math.collapseAABB3(); this._state = new RenderState({ obb: math.OBB3(), numInstances: 0, origin: cfg.origin ? math.vec3(cfg.origin) : null, geometry: cfg.geometry, positionsDecodeMatrix: cfg.geometry.positionsDecodeMatrix, // So we can null the geometry for GC colorsBuf: null, flagsBuf: null, offsetsBuf: null, modelMatrixCol0Buf: null, modelMatrixCol1Buf: null, modelMatrixCol2Buf: null, pickColorsBuf: null }); // These counts are used to avoid unnecessary render passes this._numPortions = 0; this._numVisibleLayerPortions = 0; this._numTransparentLayerPortions = 0; this._numXRayedLayerPortions = 0; this._numHighlightedLayerPortions = 0; this._numSelectedLayerPortions = 0; this._numClippableLayerPortions = 0; this._numEdgesLayerPortions = 0; this._numPickableLayerPortions = 0; this._numCulledLayerPortions = 0; /** @private */ this.numIndices = cfg.geometry.numIndices; // Per-instance arrays this._pickColors = []; this._offsets = []; // Modeling matrix per instance, array for each column this._modelMatrixCol0 = []; this._modelMatrixCol1 = []; this._modelMatrixCol2 = []; this._portions = []; this._meshes = []; this._aabb = math.collapseAABB3(); this.aabbDirty = true; this._finalized = false; /** * The type of primitives in this layer. */ this.primitive = cfg.geometry.primitive; } get aabb() { if (this.aabbDirty) { math.collapseAABB3(this._aabb); for (let i = 0, len = this._meshes.length; i < len; i++) { math.expandAABB3(this._aabb, this._meshes[i].aabb); } this.aabbDirty = false; } return this._aabb; } /** * Creates a new portion within this InstancingLayer, returns the new portion ID. * * The portion will instance this InstancingLayer's geometry. * * Gives the portion the specified color and matrix. * * @param mesh The SceneModelMesh that owns the portion * @param cfg Portion params * @param cfg.meshMatrix Flat float 4x4 matrix. * @param [cfg.worldMatrix] Flat float 4x4 matrix. * @param cfg.pickColor Quantized pick color * @returns {number} Portion ID. */ createPortion(mesh, cfg) { const meshMatrix = cfg.meshMatrix; const pickColor = cfg.pickColor; if (this._finalized) { throw "Already finalized"; } if (this.model.scene.entityOffsetsEnabled) { this._offsets.push(0); this._offsets.push(0); this._offsets.push(0); } this._modelMatrixCol0.push(meshMatrix[0]); this._modelMatrixCol0.push(meshMatrix[4]); this._modelMatrixCol0.push(meshMatrix[8]); this._modelMatrixCol0.push(meshMatrix[12]); this._modelMatrixCol1.push(meshMatrix[1]); this._modelMatrixCol1.push(meshMatrix[5]); this._modelMatrixCol1.push(meshMatrix[9]); this._modelMatrixCol1.push(meshMatrix[13]); this._modelMatrixCol2.push(meshMatrix[2]); this._modelMatrixCol2.push(meshMatrix[6]); this._modelMatrixCol2.push(meshMatrix[10]); this._modelMatrixCol2.push(meshMatrix[14]); // Per-instance pick colors this._pickColors.push(pickColor[0]); this._pickColors.push(pickColor[1]); this._pickColors.push(pickColor[2]); this._pickColors.push(pickColor[3]); this._state.numInstances++; const portionId = this._portions.length; this._portions.push({}); this._numPortions++; this.model.numPortions++; this._meshes.push(mesh); return portionId; } finalize() { if (this._finalized) { throw "Already finalized"; } const gl = this.model.scene.canvas.gl; const flagsLength = this._pickColors.length / 4; const state = this._state; const geometry = state.geometry; if (flagsLength > 0) { // Because we only build flags arrays here, // get their length from the colors array let notNormalized = false; state.flagsBuf = new ArrayBuf(gl, gl.ARRAY_BUFFER, new Float32Array(flagsLength), flagsLength, 1, gl.DYNAMIC_DRAW, notNormalized); } if (this.model.scene.entityOffsetsEnabled) { if (this._offsets.length > 0) { const notNormalized = false; state.offsetsBuf = new ArrayBuf(gl, gl.ARRAY_BUFFER, new Float32Array(this._offsets), this._offsets.length, 3, gl.DYNAMIC_DRAW, notNormalized); this._offsets = []; // Release memory } } if (geometry.positionsCompressed && geometry.positionsCompressed.length > 0) { const normalized = false; state.positionsBuf = new ArrayBuf(gl, gl.ARRAY_BUFFER, geometry.positionsCompressed, geometry.positionsCompressed.length, 3, gl.STATIC_DRAW, normalized); state.positionsDecodeMatrix = math.mat4(geometry.positionsDecodeMatrix); } if (geometry.colorsCompressed && geometry.colorsCompressed.length > 0) { const colorsCompressed = new Uint8Array(geometry.colorsCompressed); const notNormalized = false; state.colorsBuf = new ArrayBuf(gl, gl.ARRAY_BUFFER, colorsCompressed, colorsCompressed.length, 4, gl.STATIC_DRAW, notNormalized); } if (this._modelMatrixCol0.length > 0) { const normalized = false; state.modelMatrixCol0Buf = new ArrayBuf(gl, gl.ARRAY_BUFFER, new Float32Array(this._modelMatrixCol0), this._modelMatrixCol0.length, 4, gl.STATIC_DRAW, normalized); state.modelMatrixCol1Buf = new ArrayBuf(gl, gl.ARRAY_BUFFER, new Float32Array(this._modelMatrixCol1), this._modelMatrixCol1.length, 4, gl.STATIC_DRAW, normalized); state.modelMatrixCol2Buf = new ArrayBuf(gl, gl.ARRAY_BUFFER, new Float32Array(this._modelMatrixCol2), this._modelMatrixCol2.length, 4, gl.STATIC_DRAW, normalized); this._modelMatrixCol0 = []; this._modelMatrixCol1 = []; this._modelMatrixCol2 = []; } if (this._pickColors.length > 0) { const normalized = false; state.pickColorsBuf = new ArrayBuf(gl, gl.ARRAY_BUFFER, new Uint8Array(this._pickColors), this._pickColors.length, 4, gl.STATIC_DRAW, normalized); this._pickColors = []; // Release memory } state.geometry = null; this._finalized = true; } // The following setters are called by VBOSceneModelMesh, in turn called by VBOSceneModelNode, only after the layer is finalized. // It's important that these are called after finalize() in order to maintain integrity of counts like _numVisibleLayerPortions etc. initFlags(portionId, flags, meshTransparent) { if (flags & ENTITY_FLAGS.VISIBLE) { this._numVisibleLayerPortions++; this.model.numVisibleLayerPortions++; } if (flags & ENTITY_FLAGS.HIGHLIGHTED) { this._numHighlightedLayerPortions++; this.model.numHighlightedLayerPortions++; } if (flags & ENTITY_FLAGS.XRAYED) { this._numXRayedLayerPortions++; this.model.numXRayedLayerPortions++; } if (flags & ENTITY_FLAGS.SELECTED) { this._numSelectedLayerPortions++; this.model.numSelectedLayerPortions++; } if (flags & ENTITY_FLAGS.CLIPPABLE) { this._numClippableLayerPortions++; this.model.numClippableLayerPortions++; } if (flags & ENTITY_FLAGS.EDGES) { this._numEdgesLayerPortions++; this.model.numEdgesLayerPortions++; } if (flags & ENTITY_FLAGS.PICKABLE) { this._numPickableLayerPortions++; this.model.numPickableLayerPortions++; } if (flags & ENTITY_FLAGS.CULLED) { this._numCulledLayerPortions++; this.model.numCulledLayerPortions++; } if (meshTransparent) { this._numTransparentLayerPortions++; this.model.numTransparentLayerPortions++; } this._setFlags(portionId, flags, meshTransparent); } setVisible(portionId, flags, meshTransparent) { if (!this._finalized) { throw "Not finalized"; } if (flags & ENTITY_FLAGS.VISIBLE) { this._numVisibleLayerPortions++; this.model.numVisibleLayerPortions++; } else { this._numVisibleLayerPortions--; this.model.numVisibleLayerPortions--; } this._setFlags(portionId, flags, meshTransparent); } setHighlighted(portionId, flags, meshTransparent) { if (!this._finalized) { throw "Not finalized"; } if (flags & ENTITY_FLAGS.HIGHLIGHTED) { this._numHighlightedLayerPortions++; this.model.numHighlightedLayerPortions++; } else { this._numHighlightedLayerPortions--; this.model.numHighlightedLayerPortions--; } this._setFlags(portionId, flags, meshTransparent); } setXRayed(portionId, flags, meshTransparent) { if (!this._finalized) { throw "Not finalized"; } if (flags & ENTITY_FLAGS.XRAYED) { this._numXRayedLayerPortions++; this.model.numXRayedLayerPortions++; } else { this._numXRayedLayerPortions--; this.model.numXRayedLayerPortions--; } this._setFlags(portionId, flags, meshTransparent); } setSelected(portionId, flags, meshTransparent) { if (!this._finalized) { throw "Not finalized"; } if (flags & ENTITY_FLAGS.SELECTED) { this._numSelectedLayerPortions++; this.model.numSelectedLayerPortions++; } else { this._numSelectedLayerPortions--; this.model.numSelectedLayerPortions--; } this._setFlags(portionId, flags, meshTransparent); } setEdges(portionId, flags, meshTransparent) { if (!this._finalized) { throw "Not finalized"; } if (flags & ENTITY_FLAGS.EDGES) { this._numEdgesLayerPortions++; this.model.numEdgesLayerPortions++; } else { this._numEdgesLayerPortions--; this.model.numEdgesLayerPortions--; } this._setFlags(portionId, flags, meshTransparent); } setClippable(portionId, flags) { if (!this._finalized) { throw "Not finalized"; } if (flags & ENTITY_FLAGS.CLIPPABLE) { this._numClippableLayerPortions++; this.model.numClippableLayerPortions++; } else { this._numClippableLayerPortions--; this.model.numClippableLayerPortions--; } this._setFlags(portionId, flags); } setCollidable(portionId, flags) { if (!this._finalized) { throw "Not finalized"; } } setPickable(portionId, flags, meshTransparent) { if (!this._finalized) { throw "Not finalized"; } if (flags & ENTITY_FLAGS.PICKABLE) { this._numPickableLayerPortions++; this.model.numPickableLayerPortions++; } else { this._numPickableLayerPortions--; this.model.numPickableLayerPortions--; } this._setFlags(portionId, flags, meshTransparent); } setCulled(portionId, flags, meshTransparent) { if (!this._finalized) { throw "Not finalized"; } if (flags & ENTITY_FLAGS.CULLED) { this._numCulledLayerPortions++; this.model.numCulledLayerPortions++; } else { this._numCulledLayerPortions--; this.model.numCulledLayerPortions--; } this._setFlags(portionId, flags, meshTransparent); } setColor(portionId, color) { // RGBA color is normalized as ints if (!this._finalized) { throw "Not finalized"; } tempUint8Vec4[0] = color[0]; tempUint8Vec4[1] = color[1]; tempUint8Vec4[2] = color[2]; this._state.colorsBuf.setData(tempUint8Vec4, portionId * 3); } setTransparent(portionId, flags, transparent) { if (transparent) { this._numTransparentLayerPortions++; this.model.numTransparentLayerPortions++; } else { this._numTransparentLayerPortions--; this.model.numTransparentLayerPortions--; } this._setFlags(portionId, flags, transparent); } // setMatrix(portionId, matrix) { //////////////////////////////////////// // TODO: Update portion matrix //////////////////////////////////////// // // if (!this._finalized) { // throw "Not finalized"; // } // // var offset = portionId * 4; // // tempFloat32Vec4[0] = matrix[0]; // tempFloat32Vec4[1] = matrix[4]; // tempFloat32Vec4[2] = matrix[8]; // tempFloat32Vec4[3] = matrix[12]; // // this._state.modelMatrixCol0Buf.setData(tempFloat32Vec4, offset); // // tempFloat32Vec4[0] = matrix[1]; // tempFloat32Vec4[1] = matrix[5]; // tempFloat32Vec4[2] = matrix[9]; // tempFloat32Vec4[3] = matrix[13]; // // this._state.modelMatrixCol1Buf.setData(tempFloat32Vec4, offset); // // tempFloat32Vec4[0] = matrix[2]; // tempFloat32Vec4[1] = matrix[6]; // tempFloat32Vec4[2] = matrix[10]; // tempFloat32Vec4[3] = matrix[14]; // // this._state.modelMatrixCol2Buf.setData(tempFloat32Vec4, offset); // } /** * flags are 4bits values encoded on a 32bit base. color flag on the first 4 bits, silhouette flag on the next 4 bits and so on for edge, pick and clippable. */ _setFlags(portionId, flags, meshTransparent) { if (!this._finalized) { throw "Not finalized"; } const visible = !!(flags & ENTITY_FLAGS.VISIBLE); const xrayed = !!(flags & ENTITY_FLAGS.XRAYED); const highlighted = !!(flags & ENTITY_FLAGS.HIGHLIGHTED); const selected = !!(flags & ENTITY_FLAGS.SELECTED); const edges = !!(flags & ENTITY_FLAGS.EDGES); const pickable = !!(flags & ENTITY_FLAGS.PICKABLE); const culled = !!(flags & ENTITY_FLAGS.CULLED); let colorFlag; if (!visible || culled || xrayed || (highlighted && !this.model.scene.highlightMaterial.glowThrough) || (selected && !this.model.scene.selectedMaterial.glowThrough)) { colorFlag = RENDER_PASSES.NOT_RENDERED; } else { if (meshTransparent) { colorFlag = RENDER_PASSES.COLOR_TRANSPARENT; } else { colorFlag = RENDER_PASSES.COLOR_OPAQUE; } } let silhouetteFlag; if (!visible || culled) { silhouetteFlag = RENDER_PASSES.NOT_RENDERED; } else if (selected) { silhouetteFlag = RENDER_PASSES.SILHOUETTE_SELECTED; } else if (highlighted) { silhouetteFlag = RENDER_PASSES.SILHOUETTE_HIGHLIGHTED; } else if (xrayed) { silhouetteFlag = RENDER_PASSES.SILHOUETTE_XRAYED; } else { silhouetteFlag = RENDER_PASSES.NOT_RENDERED; } let edgeFlag = 0; if (!visible || culled) { edgeFlag = RENDER_PASSES.NOT_RENDERED; } else if (selected) { edgeFlag = RENDER_PASSES.EDGES_SELECTED; } else if (highlighted) { edgeFlag = RENDER_PASSES.EDGES_HIGHLIGHTED; } else if (xrayed) { edgeFlag = RENDER_PASSES.EDGES_XRAYED; } else if (edges) { if (meshTransparent) { edgeFlag = RENDER_PASSES.EDGES_COLOR_TRANSPARENT; } else { edgeFlag = RENDER_PASSES.EDGES_COLOR_OPAQUE; } } else { edgeFlag = RENDER_PASSES.NOT_RENDERED; } let pickFlag = (visible && !culled && pickable) ? RENDER_PASSES.PICK : RENDER_PASSES.NOT_RENDERED; const clippableFlag = !!(flags & ENTITY_FLAGS.CLIPPABLE) ? 255 : 0; let vertFlag = 0; vertFlag |= colorFlag; vertFlag |= silhouetteFlag << 4; vertFlag |= edgeFlag << 8; vertFlag |= pickFlag << 12; vertFlag |= clippableFlag << 16; tempFloat32[0] = vertFlag; this._state.flagsBuf.setData(tempFloat32, portionId); } setOffset(portionId, offset) { if (!this._finalized) { throw "Not finalized"; } if (!this.model.scene.entityOffsetsEnabled) { this.model.error("Entity#offset not enabled for this Viewer"); // See Viewer entityOffsetsEnabled return; } tempVec3fa[0] = offset[0]; tempVec3fa[1] = offset[1]; tempVec3fa[2] = offset[2]; this._state.offsetsBuf.setData(tempVec3fa, portionId * 3); } setMatrix(portionId, matrix) { if (!this._finalized) { throw "Not finalized"; } const offset = portionId * 4; tempFloat32Vec4[0] = matrix[0]; tempFloat32Vec4[1] = matrix[4]; tempFloat32Vec4[2] = matrix[8]; tempFloat32Vec4[3] = matrix[12]; this._state.modelMatrixCol0Buf.setData(tempFloat32Vec4, offset); tempFloat32Vec4[0] = matrix[1]; tempFloat32Vec4[1] = matrix[5]; tempFloat32Vec4[2] = matrix[9]; tempFloat32Vec4[3] = matrix[13]; this._state.modelMatrixCol1Buf.setData(tempFloat32Vec4, offset); tempFloat32Vec4[0] = matrix[2]; tempFloat32Vec4[1] = matrix[6]; tempFloat32Vec4[2] = matrix[10]; tempFloat32Vec4[3] = matrix[14]; this._state.modelMatrixCol2Buf.setData(tempFloat32Vec4, offset); } // ---------------------- NORMAL RENDERING ----------------------------------- drawColorOpaque(renderFlags, frameCtx) { if (this._numCulledLayerPortions === this._numPortions || this._numVisibleLayerPortions === 0 || this._numTransparentLayerPortions === this._numPortions || this._numXRayedLayerPortions === this._numPortions) { return; } if (this._renderers.colorRenderer) { this._renderers.colorRenderer.drawLayer(frameCtx, this, RENDER_PASSES.COLOR_OPAQUE); } } drawColorTransparent(renderFlags, frameCtx) { if (this._numCulledLayerPortions === this._numPortions || this._numVisibleLayerPortions === 0 || this._numTransparentLayerPortions === 0 || this._numXRayedLayerPortions === this._numPortions) { return; } if (this._renderers.colorRenderer) { this._renderers.colorRenderer.drawLayer(frameCtx, this, RENDER_PASSES.COLOR_TRANSPARENT); } } // -- RENDERING SAO POST EFFECT TARGETS ---------------------------------------------------------------------------- drawDepth(renderFlags, frameCtx) { } drawNormals(renderFlags, frameCtx) { } // ---------------------- EMPHASIS RENDERING ----------------------------------- drawSilhouetteXRayed(renderFlags, frameCtx) { if (this._numCulledLayerPortions === this._numPortions || this._numVisibleLayerPortions === 0 || this._numXRayedLayerPortions === 0) { return; } if (this._renderers.silhouetteRenderer) { this._renderers.silhouetteRenderer.drawLayer(frameCtx, this, RENDER_PASSES.SILHOUETTE_XRAYED); } } drawSilhouetteHighlighted(renderFlags, frameCtx) { if (this._numCulledLayerPortions === this._numPortions || this._numVisibleLayerPortions === 0 || this._numHighlightedLayerPortions === 0) { return; } if (this._renderers.silhouetteRenderer) { this._renderers.silhouetteRenderer.drawLayer(frameCtx, this, RENDER_PASSES.SILHOUETTE_HIGHLIGHTED); } } drawSilhouetteSelected(renderFlags, frameCtx) { if (this._numCulledLayerPortions === this._numPortions || this._numVisibleLayerPortions === 0 || this._numSelectedLayerPortions === 0) { return; } if (this._renderers.silhouetteRenderer) { this._renderers.silhouetteRenderer.drawLayer(frameCtx, this, RENDER_PASSES.SILHOUETTE_SELECTED); } } //-- EDGES RENDERING ----------------------------------------------------------------------------------------------- drawEdgesColorOpaque(renderFlags, frameCtx) { } drawEdgesColorTransparent(renderFlags, frameCtx) { } drawEdgesHighlighted(renderFlags, frameCtx) { } drawEdgesSelected(renderFlags, frameCtx) { } drawEdgesXRayed(renderFlags, frameCtx) { } // ---------------------- OCCLUSION CULL RENDERING ----------------------------------- drawOcclusion(renderFlags, frameCtx) { if (this._numCulledLayerPortions === this._numPortions || this._numVisibleLayerPortions === 0) { return; } if (this._renderers.occlusionRenderer) { // Only opaque, filled objects can be occluders this._renderers.occlusionRenderer.drawLayer(frameCtx, this, RENDER_PASSES.COLOR_OPAQUE); } } // ---------------------- SHADOW BUFFER RENDERING ----------------------------------- drawShadow(renderFlags, frameCtx) { } //---- PICKING ---------------------------------------------------------------------------------------------------- drawPickMesh(renderFlags, frameCtx) { if (this._numCulledLayerPortions === this._numPortions || this._numVisibleLayerPortions === 0) { return; } if (this._renderers.pickMeshRenderer) { this._renderers.pickMeshRenderer.drawLayer(frameCtx, this, RENDER_PASSES.PICK); } } drawPickDepths(renderFlags, frameCtx) { if (this._numCulledLayerPortions === this._numPortions || this._numVisibleLayerPortions === 0) { return; } if (this._renderers.pickDepthRenderer) { this._renderers.pickDepthRenderer.drawLayer(frameCtx, this, RENDER_PASSES.PICK); } } drawPickNormals(renderFlags, frameCtx) { } drawSnapInit(renderFlags, frameCtx) { if (this._numCulledLayerPortions === this._numPortions || this._numVisibleLayerPortions === 0) { return; } if (this._renderers.snapInitRenderer) { this._renderers.snapInitRenderer.drawLayer(frameCtx, this, RENDER_PASSES.PICK); } } drawSnap(renderFlags, frameCtx) { if (this._numCulledLayerPortions === this._numPortions || this._numVisibleLayerPortions === 0) { return; } if (this._renderers.snapRenderer) { this._renderers.snapRenderer.drawLayer(frameCtx, this, RENDER_PASSES.PICK); } } destroy() { const state = this._state; if (state.colorsBuf) { state.colorsBuf.destroy(); state.colorsBuf = null; } if (state.flagsBuf) { state.flagsBuf.destroy(); state.flagsBuf = null; } if (state.offsetsBuf) { state.offsetsBuf.destroy(); state.offsetsBuf = null; } if (state.modelMatrixCol0Buf) { state.modelMatrixCol0Buf.destroy(); state.modelMatrixCol0Buf = null; } if (state.modelMatrixCol1Buf) { state.modelMatrixCol1Buf.destroy(); state.modelMatrixCol1Buf = null; } if (state.modelMatrixCol2Buf) { state.modelMatrixCol2Buf.destroy(); state.modelMatrixCol2Buf = null; } if (state.pickColorsBuf) { state.pickColorsBuf.destroy(); state.pickColorsBuf = null; } state.destroy(); } } const tempVec3a$q = math.vec3(); const tempVec3b$m = math.vec3(); const tempMat4a$e = math.mat4(); /** * @private */ class DTXLinesColorRenderer { constructor(scene) { this._scene = scene; this._hash = this._getHash(); this._allocate(); } getValid() { return this._hash === this._getHash(); }; _getHash() { return this._scene._sectionPlanesState.getHash(); } drawLayer(frameCtx, dataTextureLayer, renderPass) { const scene = this._scene; const camera = scene.camera; const model = dataTextureLayer.model; const gl = scene.canvas.gl; const state = dataTextureLayer._state; const textureState = state.textureState; const origin = dataTextureLayer._state.origin; const {position, rotationMatrix} = model; const viewMatrix = camera.viewMatrix; if (!this._program) { this._allocate(); if (this.errors) { return; } } if (frameCtx.lastProgramId !== this._program.id) { frameCtx.lastProgramId = this._program.id; this._bindProgram(frameCtx, state); } textureState.bindCommonTextures( this._program, this.uPerObjectDecodeMatrix, this._uPerVertexPosition, this.uPerObjectColorAndFlags, this._uPerObjectMatrix ); let rtcViewMatrix; const gotOrigin = (origin[0] !== 0 || origin[1] !== 0 || origin[2] !== 0); const gotPosition = (position[0] !== 0 || position[1] !== 0 || position[2] !== 0); if (gotOrigin || gotPosition) { const rtcOrigin = tempVec3a$q; if (gotOrigin) { const rotatedOrigin = math.transformPoint3(rotationMatrix, origin, tempVec3b$m); rtcOrigin[0] = rotatedOrigin[0]; rtcOrigin[1] = rotatedOrigin[1]; rtcOrigin[2] = rotatedOrigin[2]; } else { rtcOrigin[0] = 0; rtcOrigin[1] = 0; rtcOrigin[2] = 0; } rtcOrigin[0] += position[0]; rtcOrigin[1] += position[1]; rtcOrigin[2] += position[2]; rtcViewMatrix = createRTCViewMat(viewMatrix, rtcOrigin, tempMat4a$e); } else { rtcViewMatrix = viewMatrix; } gl.uniformMatrix4fv(this._uSceneModelMatrix, false, rotationMatrix); gl.uniformMatrix4fv(this._uViewMatrix, false, rtcViewMatrix); gl.uniformMatrix4fv(this._uProjMatrix, false, camera.projMatrix); gl.uniform1i(this._uRenderPass, renderPass); if (scene.logarithmicDepthBufferEnabled) { const logDepthBufFC = 2.0 / (Math.log(frameCtx.pickZFar + 1.0) / Math.LN2); gl.uniform1f(this._uLogDepthBufFC, logDepthBufFC); } const numAllocatedSectionPlanes = scene._sectionPlanesState.getNumAllocatedSectionPlanes(); const numSectionPlanes = scene._sectionPlanesState.sectionPlanes.length; if (numAllocatedSectionPlanes > 0) { const sectionPlanes = scene._sectionPlanesState.sectionPlanes; const baseIndex = dataTextureLayer.layerIndex * numSectionPlanes; const renderFlags = model.renderFlags; for (let sectionPlaneIndex = 0; sectionPlaneIndex < numAllocatedSectionPlanes; sectionPlaneIndex++) { const sectionPlaneUniforms = this._uSectionPlanes[sectionPlaneIndex]; if (sectionPlaneUniforms) { if (sectionPlaneIndex < numSectionPlanes) { const active = renderFlags.sectionPlanesActivePerLayer[baseIndex + sectionPlaneIndex]; gl.uniform1i(sectionPlaneUniforms.active, active ? 1 : 0); if (active) { const sectionPlane = sectionPlanes[sectionPlaneIndex]; if (origin) { const rtcSectionPlanePos = getPlaneRTCPos(sectionPlane.dist, sectionPlane.dir, origin, tempVec3a$q, model.matrix); gl.uniform3fv(sectionPlaneUniforms.pos, rtcSectionPlanePos); } else { gl.uniform3fv(sectionPlaneUniforms.pos, sectionPlane.pos); } gl.uniform3fv(sectionPlaneUniforms.dir, sectionPlane.dir); } } else { gl.uniform1i(sectionPlaneUniforms.active, 0); } } } } if (state.numIndices8Bits > 0) { textureState.bindLineIndicesTextures( this._program, this._uPerLineObject, this._uPerLineIndices, 8 // 8 bits indices ); gl.drawArrays(gl.LINES, 0, state.numIndices8Bits); } if (state.numIndices16Bits > 0) { textureState.bindLineIndicesTextures( this._program, this._uPerLineObject, this._uPerLineIndices, 16 // 16 bits indices ); gl.drawArrays(gl.LINES, 0, state.numIndices16Bits); } if (state.numIndices32Bits > 0) { textureState.bindLineIndicesTextures( this._program, this._uPerLineObject, this._uPerLineIndices, 32 // 32 bits indices ); gl.drawArrays(gl.LINES, 0, state.numIndices32Bits); } frameCtx.drawElements++; } _allocate() { const scene = this._scene; const gl = scene.canvas.gl; this._program = new Program(gl, this._buildShader()); if (this._program.errors) { this.errors = this._program.errors; console.error(this.errors); return; } const program = this._program; this._uRenderPass = program.getLocation("renderPass"); this._uSceneModelMatrix = program.getLocation("sceneModelMatrix"); this._uViewMatrix = program.getLocation("viewMatrix"); this._uProjMatrix = program.getLocation("projMatrix"); this._uSectionPlanes = []; for (let i = 0, len = scene._sectionPlanesState.getNumAllocatedSectionPlanes(); i < len; i++) { this._uSectionPlanes.push({ active: program.getLocation("sectionPlaneActive" + i), pos: program.getLocation("sectionPlanePos" + i), dir: program.getLocation("sectionPlaneDir" + i) }); } if (scene.logarithmicDepthBufferEnabled) { this._uLogDepthBufFC = program.getLocation("logDepthBufFC"); } this.uPerObjectDecodeMatrix = "uPerObjectDecodeMatrix"; this.uPerObjectColorAndFlags = "uPerObjectColorAndFlags"; this._uPerVertexPosition = "uPerVertexPosition"; this._uPerLineIndices = "uPerLineIndices"; this._uPerLineObject = "uPerLineObject"; this._uPerObjectMatrix= "uPerObjectMatrix"; this._uCameraEyeRtc = program.getLocation("uCameraEyeRtc"); } _bindProgram(frameCtx) { const scene = this._scene; const gl = scene.canvas.gl; const program = this._program; const project = scene.camera.project; program.bind(); if (scene.logarithmicDepthBufferEnabled) { const logDepthBufFC = 2.0 / (Math.log(project.far + 1.0) / Math.LN2); gl.uniform1f(this._uLogDepthBufFC, logDepthBufFC); } } _buildShader() { return { vertex: this._buildVertexShader(), fragment: this._buildFragmentShader() }; } _buildVertexShader() { const scene = this._scene; const clipping = scene._sectionPlanesState.getNumAllocatedSectionPlanes() > 0; const src = []; src.push("#version 300 es"); src.push("// LinesDataTextureColorRenderer"); src.push("#ifdef GL_FRAGMENT_PRECISION_HIGH"); src.push("precision highp float;"); src.push("precision highp int;"); src.push("precision highp usampler2D;"); src.push("precision highp isampler2D;"); src.push("precision highp sampler2D;"); src.push("#else"); src.push("precision mediump float;"); src.push("precision mediump int;"); src.push("precision mediump usampler2D;"); src.push("precision mediump isampler2D;"); src.push("precision mediump sampler2D;"); src.push("#endif"); src.push("uniform int renderPass;"); if (scene.entityOffsetsEnabled) ; src.push("uniform mat4 sceneModelMatrix;"); src.push("uniform mat4 viewMatrix;"); src.push("uniform mat4 projMatrix;"); src.push("uniform highp sampler2D uPerObjectDecodeMatrix;"); src.push("uniform highp sampler2D uPerObjectMatrix;"); src.push("uniform lowp usampler2D uPerObjectColorAndFlags;"); src.push("uniform mediump usampler2D uPerVertexPosition;"); src.push("uniform highp usampler2D uPerLineIndices;"); src.push("uniform mediump usampler2D uPerLineObject;"); // src.push("uniform vec4 color;"); if (scene.logarithmicDepthBufferEnabled) { src.push("uniform float logDepthBufFC;"); src.push("out float vFragDepth;"); src.push("bool isPerspectiveMatrix(mat4 m) {"); src.push(" return (m[2][3] == - 1.0);"); src.push("}"); src.push("out float isPerspective;"); } if (clipping) { src.push("out vec4 vWorldPosition;"); src.push("flat out uint vFlags2;"); } src.push("out vec4 vColor;"); src.push("void main(void) {"); src.push(" int lineIndex = gl_VertexID / 2;"); src.push(" int h_packed_object_id_index = (lineIndex >> 3) & 4095;"); src.push(" int v_packed_object_id_index = (lineIndex >> 3) >> 12;"); src.push(" int objectIndex = int(texelFetch(uPerLineObject, ivec2(h_packed_object_id_index, v_packed_object_id_index), 0).r);"); src.push(" ivec2 objectIndexCoords = ivec2(objectIndex % 512, objectIndex / 512);"); src.push(" uvec4 flags = texelFetch (uPerObjectColorAndFlags, ivec2(objectIndexCoords.x*8+2, objectIndexCoords.y), 0);"); src.push(" uvec4 flags2 = texelFetch (uPerObjectColorAndFlags, ivec2(objectIndexCoords.x*8+3, objectIndexCoords.y), 0);"); // flags.x = NOT_RENDERED | COLOR_OPAQUE | COLOR_TRANSPARENT // renderPass = COLOR_OPAQUE src.push(` if (int(flags.x) != renderPass) {`); src.push(" gl_Position = vec4(3.0, 3.0, 3.0, 1.0);"); // Cull vertex src.push(" return;"); // Cull vertex src.push(" } else {"); src.push(" ivec4 packedVertexBase = ivec4(texelFetch (uPerObjectColorAndFlags, ivec2(objectIndexCoords.x*8+4, objectIndexCoords.y), 0));"); src.push(" ivec4 packedLineIndexBaseOffset = ivec4(texelFetch (uPerObjectColorAndFlags, ivec2(objectIndexCoords.x*8+6, objectIndexCoords.y), 0));"); src.push(" int lineIndexBaseOffset = (packedLineIndexBaseOffset.r << 24) + (packedLineIndexBaseOffset.g << 16) + (packedLineIndexBaseOffset.b << 8) + packedLineIndexBaseOffset.a;"); src.push(" int h_index = (lineIndex - lineIndexBaseOffset) & 4095;"); src.push(" int v_index = (lineIndex - lineIndexBaseOffset) >> 12;"); src.push(" ivec3 vertexIndices = ivec3(texelFetch(uPerLineIndices, ivec2(h_index, v_index), 0));"); src.push(" ivec3 uniqueVertexIndexes = vertexIndices + (packedVertexBase.r << 24) + (packedVertexBase.g << 16) + (packedVertexBase.b << 8) + packedVertexBase.a;"); src.push(" int indexPositionH = uniqueVertexIndexes[gl_VertexID % 2] & 4095;"); src.push(" int indexPositionV = uniqueVertexIndexes[gl_VertexID % 2] >> 12;"); src.push(" mat4 objectInstanceMatrix = mat4 (texelFetch (uPerObjectMatrix, ivec2(objectIndexCoords.x*4+0, objectIndexCoords.y), 0), texelFetch (uPerObjectMatrix, ivec2(objectIndexCoords.x*4+1, objectIndexCoords.y), 0), texelFetch (uPerObjectMatrix, ivec2(objectIndexCoords.x*4+2, objectIndexCoords.y), 0), texelFetch (uPerObjectMatrix, ivec2(objectIndexCoords.x*4+3, objectIndexCoords.y), 0));"); src.push(" mat4 objectDecodeAndInstanceMatrix = objectInstanceMatrix * mat4 (texelFetch (uPerObjectDecodeMatrix, ivec2(objectIndexCoords.x*4+0, objectIndexCoords.y), 0), texelFetch (uPerObjectDecodeMatrix, ivec2(objectIndexCoords.x*4+1, objectIndexCoords.y), 0), texelFetch (uPerObjectDecodeMatrix, ivec2(objectIndexCoords.x*4+2, objectIndexCoords.y), 0), texelFetch (uPerObjectDecodeMatrix, ivec2(objectIndexCoords.x*4+3, objectIndexCoords.y), 0));"); src.push(" uvec4 flags = texelFetch (uPerObjectColorAndFlags, ivec2(objectIndexCoords.x*8+2, objectIndexCoords.y), 0);"); src.push(" uvec4 flags2 = texelFetch (uPerObjectColorAndFlags, ivec2(objectIndexCoords.x*8+3, objectIndexCoords.y), 0);"); src.push(" vec3 position = vec3(texelFetch(uPerVertexPosition, ivec2(indexPositionH, indexPositionV), 0));"); src.push(" uvec4 color = texelFetch (uPerObjectColorAndFlags, ivec2(objectIndexCoords.x*8+0, objectIndexCoords.y), 0);"); src.push(` if (color.a == 0u) {`); src.push(" gl_Position = vec4(3.0, 3.0, 3.0, 1.0);"); // Cull vertex src.push(" return;"); src.push(" };"); src.push(" vec4 worldPosition = sceneModelMatrix * (objectDecodeAndInstanceMatrix * vec4(position, 1.0)); "); src.push(" vec4 viewPosition = viewMatrix * worldPosition; "); if (clipping) { src.push(" vWorldPosition = worldPosition;"); src.push(" vFlags2 = flags2.r;"); } src.push(" vec4 clipPos = projMatrix * viewPosition;"); if (scene.logarithmicDepthBufferEnabled) { src.push(" vFragDepth = 1.0 + clipPos.w;"); src.push(" isPerspective = float (isPerspectiveMatrix(projMatrix));"); } src.push(" gl_Position = clipPos;"); src.push(" vec4 rgb = vec4(color.rgba);"); src.push(" vColor = vec4(float(rgb.r*0.5) / 255.0, float(rgb.g*0.5) / 255.0, float(rgb.b*0.5) / 255.0, float(rgb.a) / 255.0);"); src.push(" }"); src.push("}"); return src; } _buildFragmentShader() { const scene = this._scene; const sectionPlanesState = scene._sectionPlanesState; const clipping = sectionPlanesState.getNumAllocatedSectionPlanes() > 0; const src = []; src.push('#version 300 es'); src.push("// LinesDataTextureColorRenderer fragment shader"); src.push("#ifdef GL_FRAGMENT_PRECISION_HIGH"); src.push("precision highp float;"); src.push("precision highp int;"); src.push("#else"); src.push("precision mediump float;"); src.push("precision mediump int;"); src.push("#endif"); if (scene.logarithmicDepthBufferEnabled) { src.push("in float isPerspective;"); src.push("uniform float logDepthBufFC;"); src.push("in float vFragDepth;"); } if (clipping) { src.push("in vec4 vWorldPosition;"); src.push("flat in uint vFlags2;"); for (let i = 0, len = sectionPlanesState.getNumAllocatedSectionPlanes(); i < len; i++) { src.push("uniform bool sectionPlaneActive" + i + ";"); src.push("uniform vec3 sectionPlanePos" + i + ";"); src.push("uniform vec3 sectionPlaneDir" + i + ";"); } } src.push("in vec4 vColor;"); src.push("out vec4 outColor;"); src.push("void main(void) {"); if (clipping) { src.push(" bool clippable = vFlags2 > 0u;"); src.push(" if (clippable) {"); src.push(" float dist = 0.0;"); for (let i = 0, len = sectionPlanesState.getNumAllocatedSectionPlanes(); i < len; i++) { src.push("if (sectionPlaneActive" + i + ") {"); src.push(" dist += clamp(dot(-sectionPlaneDir" + i + ".xyz, vWorldPosition.xyz - sectionPlanePos" + i + ".xyz), 0.0, 1000.0);"); src.push("}"); } src.push(" if (dist > 0.0) { "); src.push(" discard;"); src.push(" }"); src.push("}"); } if (scene.logarithmicDepthBufferEnabled) { src.push(" gl_FragDepth = isPerspective == 0.0 ? gl_FragCoord.z : log2( vFragDepth ) * logDepthBufFC * 0.5;"); } src.push(" outColor = vColor;"); src.push("}"); return src; } webglContextRestored() { this._program = null; } destroy() { if (this._program) { this._program.destroy(); } this._program = null; } } /** * @private */ class DTXLinesRenderers { constructor(scene) { this._scene = scene; } _compile() { if (this._colorRenderer && (!this._colorRenderer.getValid())) { this._colorRenderer.destroy(); this._colorRenderer = null; } } eagerCreateRenders() { // Pre-initialize certain renderers that would otherwise be lazy-initialised // on user interaction, such as picking or emphasis, so that there is no delay // when user first begins interacting with the viewer. } get colorRenderer() { if (!this._colorRenderer) { this._colorRenderer = new DTXLinesColorRenderer(this._scene, false); } return this._colorRenderer; } _destroy() { if (this._colorRenderer) { this._colorRenderer.destroy(); } } } const cachedRenderers = {}; /** * @private */ function getRenderers$1(scene) { const sceneId = scene.id; let dataTextureRenderers = cachedRenderers[sceneId]; if (!dataTextureRenderers) { dataTextureRenderers = new DTXLinesRenderers(scene); cachedRenderers[sceneId] = dataTextureRenderers; dataTextureRenderers._compile(); dataTextureRenderers.eagerCreateRenders(); scene.on("compile", () => { dataTextureRenderers._compile(); dataTextureRenderers.eagerCreateRenders(); }); scene.on("destroyed", () => { delete cachedRenderers[sceneId]; dataTextureRenderers._destroy(); }); } return dataTextureRenderers; } /** * @private */ class DTXLinesBuffer { constructor() { this.positionsCompressed = []; this.lenPositionsCompressed = 0; this.indices8Bits = []; this.lenIndices8Bits = 0; this.indices16Bits = []; this.lenIndices16Bits = 0; this.indices32Bits = []; this.lenIndices32Bits = 0; this.perObjectColors = []; this.perObjectPickColors = []; this.perObjectSolid = []; this.perObjectOffsets = []; this.perObjectPositionsDecodeMatrices = []; this.perObjectInstancePositioningMatrices = []; this.perObjectVertexBases = []; this.perObjectIndexBaseOffsets = []; this.perLineNumberPortionId8Bits = []; this.perLineNumberPortionId16Bits = []; this.perLineNumberPortionId32Bits = []; } } /** * @private */ class DTXLinesState { constructor() { this.texturePerObjectColorsAndFlags = null; this.texturePerObjectOffsets = null; this.texturePerObjectInstanceMatrices = null; this.texturePerObjectPositionsDecodeMatrix = null; this.texturePerVertexIdCoordinates = null; this.texturePerLineIdPortionIds8Bits = null; this.texturePerLineIdPortionIds16Bits = null; this.texturePerLineIdPortionIds32Bits = null; this.texturePerLineIdIndices8Bits = null; this.texturePerLineIdIndices16Bits = null; this.texturePerLineIdIndices32Bits = null; this.textureModelMatrices = null; } finalize() { this.indicesPerBitnessTextures = { 8: this.texturePerLineIdIndices8Bits, 16: this.texturePerLineIdIndices16Bits, 32: this.texturePerLineIdIndices32Bits, }; this.indicesPortionIdsPerBitnessTextures = { 8: this.texturePerLineIdPortionIds8Bits, 16: this.texturePerLineIdPortionIds16Bits, 32: this.texturePerLineIdPortionIds32Bits, }; } bindCommonTextures(glProgram, objectDecodeMatricesShaderName, vertexTextureShaderName, objectAttributesTextureShaderName, objectMatricesShaderName) { this.texturePerObjectPositionsDecodeMatrix.bindTexture(glProgram, objectDecodeMatricesShaderName, 1); this.texturePerVertexIdCoordinates.bindTexture(glProgram, vertexTextureShaderName, 2); this.texturePerObjectColorsAndFlags.bindTexture(glProgram, objectAttributesTextureShaderName, 3); this.texturePerObjectInstanceMatrices.bindTexture(glProgram, objectMatricesShaderName, 4); } bindLineIndicesTextures(glProgram, portionIdsShaderName, lineIndicesShaderName, textureBitness) { this.indicesPortionIdsPerBitnessTextures[textureBitness].bindTexture(glProgram, portionIdsShaderName, 5); this.indicesPerBitnessTextures[textureBitness].bindTexture(glProgram, lineIndicesShaderName, 6); } } /** * @private */ class BindableDataTexture { constructor(gl, texture, textureWidth, textureHeight, textureData = null) { this._gl = gl; this._texture = texture; this._textureWidth = textureWidth; this._textureHeight = textureHeight; this._textureData = textureData; } bindTexture(glProgram, shaderName, glTextureUnit) { return glProgram.bindTexture(shaderName, this, glTextureUnit); } bind(unit) { this._gl.activeTexture(this._gl["TEXTURE" + unit]); this._gl.bindTexture(this._gl.TEXTURE_2D, this._texture); return true; } unbind(unit) { // This `unbind` method is ignored at the moment to allow avoiding // to rebind same texture already bound to a texture unit. // this._gl.activeTexture(this.state.gl["TEXTURE" + unit]); // this._gl.bindTexture(this.state.gl.TEXTURE_2D, null); } } const dataTextureRamStats$1 = { sizeDataColorsAndFlags: 0, sizeDataPositionDecodeMatrices: 0, sizeDataTextureOffsets: 0, sizeDataTexturePositions: 0, sizeDataTextureIndices: 0, sizeDataTexturePortionIds: 0, numberOfGeometries: 0, numberOfPortions: 0, numberOfLayers: 0, numberOfTextures: 0, totalLines: 0, totalLines8Bits: 0, totalLines16Bits: 0, totalLines32Bits: 0, cannotCreatePortion: { because10BitsObjectId: 0, becauseTextureSize: 0, }, overheadSizeAlignementIndices: 0, overheadSizeAlignementEdgeIndices: 0, }; window.printDataTextureRamStats = function () { console.log(JSON.stringify(dataTextureRamStats$1, null, 4)); let totalRamSize = 0; Object.keys(dataTextureRamStats$1).forEach(key => { if (key.startsWith("size")) { totalRamSize += dataTextureRamStats$1[key]; } }); console.log(`Total size ${totalRamSize} bytes (${(totalRamSize / 1000 / 1000).toFixed(2)} MB)`); console.log(`Avg bytes / triangle: ${(totalRamSize / dataTextureRamStats$1.totalLines).toFixed(2)}`); let percentualRamStats = {}; Object.keys(dataTextureRamStats$1).forEach(key => { if (key.startsWith("size")) { percentualRamStats[key] = `${(dataTextureRamStats$1[key] / totalRamSize * 100).toFixed(2)} % of total`; } }); console.log(JSON.stringify({percentualRamUsage: percentualRamStats}, null, 4)); }; /** * @private */ class DTXLinesTextureFactory { /** * Enables the currently binded ````WebGLTexture```` to be used as a data texture. * * @param {WebGL2RenderingContext} gl * * @private */ disableBindedTextureFiltering(gl) { gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); } /** * This will generate an RGBA texture for: * - colors * - pickColors * - flags * - flags2 * - vertex bases * - vertex base offsets * * The texture will have: * - 4 RGBA columns per row: for each object (pick) color and flags(2) * - N rows where N is the number of objects * * @param {WebGL2RenderingContext} gl * @param {ArrayLike>} colors Array of colors for all objects in the layer * @param {ArrayLike>} pickColors Array of pickColors for all objects in the layer * @param {ArrayLike} vertexBases Array of position-index-bases foteh all objects in the layer * @param {ArrayLike} indexBaseOffsets For lines: array of offests between the (gl_VertexID / 2) and the position where the indices start in the texture layer * * @returns {BindableDataTexture} */ generateTextureForColorsAndFlags(gl, colors, pickColors, vertexBases, indexBaseOffsets) { const numPortions = colors.length; // The number of rows in the texture is the number of // objects in the layer. this.numPortions = numPortions; const textureWidth = 512 * 8; const textureHeight = Math.ceil(numPortions / (textureWidth / 8)); if (textureHeight === 0) { throw "texture height===0"; } // 8 columns per texture row: // - col0: (RGBA) object color RGBA // - col1: (packed Uint32 as RGBA) object pick color // - col2: (packed 4 bytes as RGBA) object flags // - col3: (packed 4 bytes as RGBA) object flags2 // - col4: (packed Uint32 bytes as RGBA) vertex base // - col5: (packed Uint32 bytes as RGBA) index base offset // // - col6: (packed Uint32 bytes as RGBA) edge index base offset // // - col7: (packed 4 bytes as RGBA) is-solid flag for objects const texArray = new Uint8Array(4 * textureWidth * textureHeight); dataTextureRamStats$1.sizeDataColorsAndFlags += texArray.byteLength; dataTextureRamStats$1.numberOfTextures++; for (let i = 0; i < numPortions; i++) { // object color texArray.set(colors [i], i * 32 + 0); texArray.set(pickColors [i], i * 32 + 4); // object pick color texArray.set([0, 0, 0, 0], i * 32 + 8); // object flags texArray.set([0, 0, 0, 0], i * 32 + 12); // object flags2 // vertex base texArray.set([ (vertexBases[i] >> 24) & 255, (vertexBases[i] >> 16) & 255, (vertexBases[i] >> 8) & 255, (vertexBases[i]) & 255, ], i * 32 + 16 ); // lines index base offset texArray.set( [ (indexBaseOffsets[i] >> 24) & 255, (indexBaseOffsets[i] >> 16) & 255, (indexBaseOffsets[i] >> 8) & 255, (indexBaseOffsets[i]) & 255, ], i * 32 + 20 ); // // edge index base offset // texArray.set( // [ // (edgeIndexBaseOffsets[i] >> 24) & 255, // (edgeIndexBaseOffsets[i] >> 16) & 255, // (edgeIndexBaseOffsets[i] >> 8) & 255, // (edgeIndexBaseOffsets[i]) & 255, // ], // i * 32 + 24 // ); // // // is-solid flag // texArray.set([solid[i] ? 1 : 0, 0, 0, 0], i * 32 + 28); } const texture = gl.createTexture(); gl.bindTexture(gl.TEXTURE_2D, texture); gl.texStorage2D(gl.TEXTURE_2D, 1, gl.RGBA8UI, textureWidth, textureHeight); gl.texSubImage2D(gl.TEXTURE_2D, 0, 0, 0, textureWidth, textureHeight, gl.RGBA_INTEGER, gl.UNSIGNED_BYTE, texArray, 0); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); gl.bindTexture(gl.TEXTURE_2D, null); return new BindableDataTexture(gl, texture, textureWidth, textureHeight, texArray); } /** * This will generate a texture for all object offsets. * * @param {WebGL2RenderingContext} gl * @param {int[]} offsets Array of int[3], one XYZ offset array for each object * * @returns {BindableDataTexture} */ generateTextureForObjectOffsets(gl, numOffsets) { const textureWidth = 512; const textureHeight = Math.ceil(numOffsets / textureWidth); if (textureHeight === 0) { throw "texture height===0"; } const texArray = new Float32Array(3 * textureWidth * textureHeight).fill(0); dataTextureRamStats$1.sizeDataTextureOffsets += texArray.byteLength; dataTextureRamStats$1.numberOfTextures++; const texture = gl.createTexture(); gl.bindTexture(gl.TEXTURE_2D, texture); gl.texStorage2D(gl.TEXTURE_2D, 1, gl.RGB32F, textureWidth, textureHeight); gl.texSubImage2D(gl.TEXTURE_2D, 0, 0, 0, textureWidth, textureHeight, gl.RGB, gl.FLOAT, texArray, 0); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); gl.bindTexture(gl.TEXTURE_2D, null); return new BindableDataTexture(gl, texture, textureWidth, textureHeight, texArray); } /** * This will generate a texture for all positions decode matrices in the layer. * * The texture will have: * - 4 RGBA columns per row (each column will contain 4 packed half-float (16 bits) components). * Thus, each row will contain 16 packed half-floats corresponding to a complete positions decode matrix) * - N rows where N is the number of objects * * @param {WebGL2RenderingContext} gl * @param {ArrayLike} instanceMatrices Array of geometry instancing matrices for all objects in the layer. Null if the objects are not instanced. * * @returns {BindableDataTexture} */ generateTextureForInstancingMatrices(gl, instanceMatrices) { const numMatrices = instanceMatrices.length; if (numMatrices === 0) { throw "num instance matrices===0"; } // in one row we can fit 512 matrices const textureWidth = 512 * 4; const textureHeight = Math.ceil(numMatrices / (textureWidth / 4)); const texArray = new Float32Array(4 * textureWidth * textureHeight); // dataTextureRamStats.sizeDataPositionDecodeMatrices += texArray.byteLength; dataTextureRamStats$1.numberOfTextures++; for (let i = 0; i < instanceMatrices.length; i++) { // 4x4 values texArray.set(instanceMatrices[i], i * 16); } const texture = gl.createTexture(); gl.bindTexture(gl.TEXTURE_2D, texture); gl.texStorage2D(gl.TEXTURE_2D, 1, gl.RGBA32F, textureWidth, textureHeight); gl.texSubImage2D(gl.TEXTURE_2D, 0, 0, 0, textureWidth, textureHeight, gl.RGBA, gl.FLOAT, texArray, 0); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); gl.bindTexture(gl.TEXTURE_2D, null); return new BindableDataTexture(gl, texture, textureWidth, textureHeight, texArray); } /** * This will generate a texture for all positions decode matrices in the layer. * * The texture will have: * - 4 RGBA columns per row (each column will contain 4 packed half-float (16 bits) components). * Thus, each row will contain 16 packed half-floats corresponding to a complete positions decode matrix) * - N rows where N is the number of objects * * @param {WebGL2RenderingContext} gl * @param {ArrayLike} positionDecodeMatrices Array of positions decode matrices for all objects in the layer * * @returns {BindableDataTexture} */ generateTextureForPositionsDecodeMatrices(gl, positionDecodeMatrices) { const numMatrices = positionDecodeMatrices.length; if (numMatrices === 0) { throw "num decode+entity matrices===0"; } // in one row we can fit 512 matrices const textureWidth = 512 * 4; const textureHeight = Math.ceil(numMatrices / (textureWidth / 4)); const texArray = new Float32Array(4 * textureWidth * textureHeight); dataTextureRamStats$1.sizeDataPositionDecodeMatrices += texArray.byteLength; dataTextureRamStats$1.numberOfTextures++; for (let i = 0; i < positionDecodeMatrices.length; i++) { // 4x4 values texArray.set(positionDecodeMatrices[i], i * 16); } const texture = gl.createTexture(); gl.bindTexture(gl.TEXTURE_2D, texture); gl.texStorage2D(gl.TEXTURE_2D, 1, gl.RGBA32F, textureWidth, textureHeight); gl.texSubImage2D(gl.TEXTURE_2D, 0, 0, 0, textureWidth, textureHeight, gl.RGBA, gl.FLOAT, texArray, 0); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); gl.bindTexture(gl.TEXTURE_2D, null); return new BindableDataTexture(gl, texture, textureWidth, textureHeight); } /** * @param {WebGL2RenderingContext} gl * @param indicesArrays * @param lenIndices * * @returns {BindableDataTexture} */ generateTextureFor8BitIndices(gl, indicesArrays, lenIndices) { if (lenIndices === 0) { return {texture: null, textureHeight: 0,}; } const textureWidth = 4096; const textureHeight = Math.ceil(lenIndices / 3 / textureWidth); if (textureHeight === 0) { throw "texture height===0"; } const texArraySize = textureWidth * textureHeight * 3; const texArray = new Uint8Array(texArraySize); dataTextureRamStats$1.sizeDataTextureIndices += texArray.byteLength; dataTextureRamStats$1.numberOfTextures++; for (let i = 0, j = 0, len = indicesArrays.length; i < len; i++) { const pc = indicesArrays[i]; texArray.set(pc, j); j += pc.length; } const texture = gl.createTexture(); gl.bindTexture(gl.TEXTURE_2D, texture); gl.texStorage2D(gl.TEXTURE_2D, 1, gl.RGB8UI, textureWidth, textureHeight); gl.texSubImage2D(gl.TEXTURE_2D, 0, 0, 0, textureWidth, textureHeight, gl.RGB_INTEGER, gl.UNSIGNED_BYTE, texArray, 0); this.disableBindedTextureFiltering(gl); gl.bindTexture(gl.TEXTURE_2D, null); return new BindableDataTexture(gl, texture, textureWidth, textureHeight); } /** * @param {WebGL2RenderingContext} gl * @param indicesArrays * @param lenIndices * * @returns {BindableDataTexture} */ generateTextureFor16BitIndices(gl, indicesArrays, lenIndices) { if (lenIndices === 0) { return {texture: null, textureHeight: 0,}; } const textureWidth = 4096; const textureHeight = Math.ceil(lenIndices / 3 / textureWidth); if (textureHeight === 0) { throw "texture height===0"; } const texArraySize = textureWidth * textureHeight * 3; const texArray = new Uint16Array(texArraySize); dataTextureRamStats$1.sizeDataTextureIndices += texArray.byteLength; dataTextureRamStats$1.numberOfTextures++; for (let i = 0, j = 0, len = indicesArrays.length; i < len; i++) { const pc = indicesArrays[i]; texArray.set(pc, j); j += pc.length; } const texture = gl.createTexture(); gl.bindTexture(gl.TEXTURE_2D, texture); gl.texStorage2D(gl.TEXTURE_2D, 1, gl.RGB16UI, textureWidth, textureHeight); gl.texSubImage2D(gl.TEXTURE_2D, 0, 0, 0, textureWidth, textureHeight, gl.RGB_INTEGER, gl.UNSIGNED_SHORT, texArray, 0); this.disableBindedTextureFiltering(gl); gl.bindTexture(gl.TEXTURE_2D, null); return new BindableDataTexture(gl, texture, textureWidth, textureHeight); } /** * @param {WebGL2RenderingContext} gl * @param indicesArrays * @param lenIndices * * @returns {BindableDataTexture} */ generateTextureFor32BitIndices(gl, indicesArrays, lenIndices) { if (lenIndices === 0) { return {texture: null, textureHeight: 0,}; } const textureWidth = 4096; const textureHeight = Math.ceil(lenIndices / 3 / textureWidth); if (textureHeight === 0) { throw "texture height===0"; } const texArraySize = textureWidth * textureHeight * 3; const texArray = new Uint32Array(texArraySize); dataTextureRamStats$1.sizeDataTextureIndices += texArray.byteLength; dataTextureRamStats$1.numberOfTextures++; for (let i = 0, j = 0, len = indicesArrays.length; i < len; i++) { const pc = indicesArrays[i]; texArray.set(pc, j); j += pc.length; } const texture = gl.createTexture(); gl.bindTexture(gl.TEXTURE_2D, texture); gl.texStorage2D(gl.TEXTURE_2D, 1, gl.RGB32UI, textureWidth, textureHeight); gl.texSubImage2D(gl.TEXTURE_2D, 0, 0, 0, textureWidth, textureHeight, gl.RGB_INTEGER, gl.UNSIGNED_INT, texArray, 0); this.disableBindedTextureFiltering(gl); gl.bindTexture(gl.TEXTURE_2D, null); return new BindableDataTexture(gl, texture, textureWidth, textureHeight); } /** * @param {WebGL2RenderingContext} gl * @param {ArrayLike} positionsArrays Arrays of quantized positions in the layer * @param lenPositions * * This will generate a texture for positions in the layer. * * The texture will have: * - 1024 columns, where each pixel will be a 16-bit-per-component RGB texture, corresponding to the XYZ of the position * - a number of rows R where R*1024 is just >= than the number of vertices (positions / 3) * * @returns {BindableDataTexture} */ generateTextureForPositions(gl, positionsArrays, lenPositions) { const numVertices = lenPositions / 3; const textureWidth = 4096; const textureHeight = Math.ceil(numVertices / textureWidth); if (textureHeight === 0) { throw "texture height===0"; } const texArraySize = textureWidth * textureHeight * 3; const texArray = new Uint16Array(texArraySize); dataTextureRamStats$1.sizeDataTexturePositions += texArray.byteLength; dataTextureRamStats$1.numberOfTextures++; for (let i = 0, j = 0, len = positionsArrays.length; i < len; i++) { const pc = positionsArrays[i]; texArray.set(pc, j); j += pc.length; } const texture = gl.createTexture(); gl.bindTexture(gl.TEXTURE_2D, texture); gl.texStorage2D(gl.TEXTURE_2D, 1, gl.RGB16UI, textureWidth, textureHeight); gl.texSubImage2D(gl.TEXTURE_2D, 0, 0, 0, textureWidth, textureHeight, gl.RGB_INTEGER, gl.UNSIGNED_SHORT, texArray, 0); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); gl.bindTexture(gl.TEXTURE_2D, null); return new BindableDataTexture(gl, texture, textureWidth, textureHeight); } /** * @param {WebGL2RenderingContext} gl * @param {ArrayLike} portionIdsArray * * @returns {BindableDataTexture} */ generateTextureForPackedPortionIds(gl, portionIdsArray) { if (portionIdsArray.length === 0) { return {texture: null, textureHeight: 0}; } const lenArray = portionIdsArray.length; const textureWidth = 4096; const textureHeight = Math.ceil(lenArray / textureWidth); if (textureHeight === 0) { throw "texture height===0"; } const texArraySize = textureWidth * textureHeight; const texArray = new Uint16Array(texArraySize); texArray.set(portionIdsArray, 0); dataTextureRamStats$1.sizeDataTexturePortionIds += texArray.byteLength; dataTextureRamStats$1.numberOfTextures++; const texture = gl.createTexture(); gl.bindTexture(gl.TEXTURE_2D, texture); gl.texStorage2D(gl.TEXTURE_2D, 1, gl.R16UI, textureWidth, textureHeight); gl.texSubImage2D(gl.TEXTURE_2D, 0, 0, 0, textureWidth, textureHeight, gl.RED_INTEGER, gl.UNSIGNED_SHORT, texArray, 0); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); gl.bindTexture(gl.TEXTURE_2D, null); return new BindableDataTexture(gl, texture, textureWidth, textureHeight); } } const configs$1 = new Configs(); const MAX_NUMBER_OF_OBJECTS_IN_LAYER$1 = (1 << 16); const MAX_DATA_TEXTURE_HEIGHT$1 = configs$1.maxDataTextureHeight; const INDICES_EDGE_INDICES_ALIGNEMENT_SIZE$1 = 8; const MAX_OBJECT_UPDATES_IN_FRAME_WITHOUT_BATCHED_UPDATE$1 = 10; const tempMat4a$d = new Float32Array(16); const tempUint8Array4$1 = new Uint8Array(4); const tempFloat32Array3$1 = new Float32Array(3); let numLayers$1 = 0; const DEFAULT_MATRIX$2 = math.identityMat4(); /** * @private */ class DTXLinesLayer { constructor(model, cfg) { // console.info("Creating DTXLinesLayer"); dataTextureRamStats$1.numberOfLayers++; this._layerNumber = numLayers$1++; this.sortId = `TriDTX-${this._layerNumber}`; // State sorting key. this.layerIndex = cfg.layerIndex; // Index of this DTXLinesLayer in {@link SceneModel#_layerList}. this._renderers = getRenderers$1(model.scene); this.model = model; this._buffer = new DTXLinesBuffer(); this._dataTextureState = new DTXLinesState(); this._dataTextureGenerator = new DTXLinesTextureFactory(); this._state = new RenderState({ origin: math.vec3(cfg.origin), textureState: this._dataTextureState, numIndices8Bits: 0, numIndices16Bits: 0, numIndices32Bits: 0, numVertices: 0, }); this._numPortions = 0; // These counts are used to avoid unnecessary render passes this._numVisibleLayerPortions = 0; this._numTransparentLayerPortions = 0; this._numXRayedLayerPortions = 0; this._numSelectedLayerPortions = 0; this._numHighlightedLayerPortions = 0; this._numClippableLayerPortions = 0; this._numPickableLayerPortions = 0; this._numCulledLayerPortions = 0; this._subPortions = []; this._portionToSubPortionsMap = []; this._bucketGeometries = {}; this._meshes = []; this._aabb = math.collapseAABB3(); this.aabbDirty = true; this._numUpdatesInFrame = 0; /** * The type of primitives in this layer. */ this.primitive = cfg.primitive; this._finalized = false; } get aabb() { if (this.aabbDirty) { math.collapseAABB3(this._aabb); for (let i = 0, len = this._meshes.length; i < len; i++) { math.expandAABB3(this._aabb, this._meshes[i].aabb); } this.aabbDirty = false; } return this._aabb; } canCreatePortion(portionCfg) { if (this._finalized) { throw "Already finalized"; } const numNewPortions = portionCfg.buckets.length; if ((this._numPortions + numNewPortions) > MAX_NUMBER_OF_OBJECTS_IN_LAYER$1) { dataTextureRamStats$1.cannotCreatePortion.because10BitsObjectId++; } let retVal = (this._numPortions + numNewPortions) <= MAX_NUMBER_OF_OBJECTS_IN_LAYER$1; const bucketIndex = 0; // TODO: Is this a bug? const bucketGeometryId = portionCfg.geometryId !== undefined && portionCfg.geometryId !== null ? `${portionCfg.geometryId}#${bucketIndex}` : `${portionCfg.id}#${bucketIndex}`; const alreadyHasPortionGeometry = this._bucketGeometries[bucketGeometryId]; if (!alreadyHasPortionGeometry) { const maxIndicesOfAnyBits = Math.max(this._state.numIndices8Bits, this._state.numIndices16Bits, this._state.numIndices32Bits,); let numVertices = 0; let numIndices = 0; portionCfg.buckets.forEach(bucket => { numVertices += bucket.positionsCompressed.length / 3; numIndices += bucket.indices.length / 2; }); if ((this._state.numVertices + numVertices) > MAX_DATA_TEXTURE_HEIGHT$1 * 4096 || (maxIndicesOfAnyBits + numIndices) > MAX_DATA_TEXTURE_HEIGHT$1 * 4096) { dataTextureRamStats$1.cannotCreatePortion.becauseTextureSize++; } retVal &&= (this._state.numVertices + numVertices) <= MAX_DATA_TEXTURE_HEIGHT$1 * 4096 && (maxIndicesOfAnyBits + numIndices) <= MAX_DATA_TEXTURE_HEIGHT$1 * 4096; } return retVal; } createPortion(mesh, portionCfg) { if (this._finalized) { throw "Already finalized"; } const subPortionIds = []; // const portionAABB = portionCfg.worldAABB; portionCfg.buckets.forEach((bucket, bucketIndex) => { const bucketGeometryId = portionCfg.geometryId !== undefined && portionCfg.geometryId !== null ? `${portionCfg.geometryId}#${bucketIndex}` : `${portionCfg.id}#${bucketIndex}`; let bucketGeometry = this._bucketGeometries[bucketGeometryId]; if (!bucketGeometry) { bucketGeometry = this._createBucketGeometry(portionCfg, bucket); this._bucketGeometries[bucketGeometryId] = bucketGeometry; } // const subPortionAABB = math.collapseAABB3(tempAABB3b); const subPortionId = this._createSubPortion(portionCfg, bucketGeometry, bucket); //math.expandAABB3(portionAABB, subPortionAABB); subPortionIds.push(subPortionId); }); const portionId = this._portionToSubPortionsMap.length; this._portionToSubPortionsMap.push(subPortionIds); this.model.numPortions++; this._meshes.push(mesh); return portionId; } _createBucketGeometry(portionCfg, bucket) { if (bucket.indices) { const alignedIndicesLen = Math.ceil((bucket.indices.length / 2) / INDICES_EDGE_INDICES_ALIGNEMENT_SIZE$1) * INDICES_EDGE_INDICES_ALIGNEMENT_SIZE$1 * 2; dataTextureRamStats$1.overheadSizeAlignementIndices += 2 * (alignedIndicesLen - bucket.indices.length); const alignedIndices = new Uint32Array(alignedIndicesLen); alignedIndices.fill(0); alignedIndices.set(bucket.indices); bucket.indices = alignedIndices; } const positionsCompressed = bucket.positionsCompressed; const indices = bucket.indices; const buffer = this._buffer; buffer.positionsCompressed.push(positionsCompressed); const vertexBase = buffer.lenPositionsCompressed / 3; const numVertices = positionsCompressed.length / 3; buffer.lenPositionsCompressed += positionsCompressed.length; let indicesBase; let numLines = 0; if (indices) { numLines = indices.length / 2; let indicesBuffer; if (numVertices <= (1 << 8)) { indicesBuffer = buffer.indices8Bits; indicesBase = buffer.lenIndices8Bits / 2; buffer.lenIndices8Bits += indices.length; } else if (numVertices <= (1 << 16)) { indicesBuffer = buffer.indices16Bits; indicesBase = buffer.lenIndices16Bits / 2; buffer.lenIndices16Bits += indices.length; } else { indicesBuffer = buffer.indices32Bits; indicesBase = buffer.lenIndices32Bits / 2; buffer.lenIndices32Bits += indices.length; } indicesBuffer.push(indices); } this._state.numVertices += numVertices; dataTextureRamStats$1.numberOfGeometries++; const bucketGeometry = { vertexBase, numVertices, numLines, indicesBase }; return bucketGeometry; } _createSubPortion(portionCfg, bucketGeometry) { const color = portionCfg.color; const colors = portionCfg.colors; const opacity = portionCfg.opacity; const meshMatrix = portionCfg.meshMatrix; const pickColor = portionCfg.pickColor; const buffer = this._buffer; const state = this._state; buffer.perObjectPositionsDecodeMatrices.push(portionCfg.positionsDecodeMatrix); buffer.perObjectInstancePositioningMatrices.push(meshMatrix || DEFAULT_MATRIX$2); buffer.perObjectSolid.push(!!portionCfg.solid); if (colors) { buffer.perObjectColors.push([colors[0] * 255, colors[1] * 255, colors[2] * 255, 255]); } else if (color) { // Color is pre-quantized by SceneModel buffer.perObjectColors.push([color[0], color[1], color[2], opacity]); } buffer.perObjectPickColors.push(pickColor); buffer.perObjectVertexBases.push(bucketGeometry.vertexBase); { let currentNumIndices; if (bucketGeometry.numVertices <= (1 << 8)) { currentNumIndices = state.numIndices8Bits; } else if (bucketGeometry.numVertices <= (1 << 16)) { currentNumIndices = state.numIndices16Bits; } else { currentNumIndices = state.numIndices32Bits; } buffer.perObjectIndexBaseOffsets.push(currentNumIndices / 2 - bucketGeometry.indicesBase); } const subPortionId = this._subPortions.length; if (bucketGeometry.numLines > 0) { let numIndices = bucketGeometry.numLines * 2; let indicesPortionIdBuffer; if (bucketGeometry.numVertices <= (1 << 8)) { indicesPortionIdBuffer = buffer.perLineNumberPortionId8Bits; state.numIndices8Bits += numIndices; dataTextureRamStats$1.totalLines8Bits += bucketGeometry.numLines; } else if (bucketGeometry.numVertices <= (1 << 16)) { indicesPortionIdBuffer = buffer.perLineNumberPortionId16Bits; state.numIndices16Bits += numIndices; dataTextureRamStats$1.totalLines16Bits += bucketGeometry.numLines; } else { indicesPortionIdBuffer = buffer.perLineNumberPortionId32Bits; state.numIndices32Bits += numIndices; dataTextureRamStats$1.totalLines32Bits += bucketGeometry.numLines; } dataTextureRamStats$1.totalLines += bucketGeometry.numLines; for (let i = 0; i < bucketGeometry.numLines; i += INDICES_EDGE_INDICES_ALIGNEMENT_SIZE$1) { indicesPortionIdBuffer.push(subPortionId); } } this._subPortions.push({ // vertsBase: vertsIndex, numVertices: bucketGeometry.numLines }); this._numPortions++; dataTextureRamStats$1.numberOfPortions++; return subPortionId; } /** * Builds data textures from the appended geometries and loads them into the GPU. * * No more portions can then be created. */ finalize() { if (this._finalized) { return; } const state = this._state; const textureState = this._dataTextureState; const gl = this.model.scene.canvas.gl; const buffer = this._buffer; state.gl = gl; textureState.texturePerObjectColorsAndFlags = this._dataTextureGenerator.generateTextureForColorsAndFlags( gl, buffer.perObjectColors, buffer.perObjectPickColors, buffer.perObjectVertexBases, buffer.perObjectIndexBaseOffsets, buffer.perObjectSolid); textureState.texturePerObjectInstanceMatrices = this._dataTextureGenerator.generateTextureForInstancingMatrices(gl, buffer.perObjectInstancePositioningMatrices); textureState.texturePerObjectPositionsDecodeMatrix = this._dataTextureGenerator.generateTextureForPositionsDecodeMatrices(gl, buffer.perObjectPositionsDecodeMatrices); textureState.texturePerVertexIdCoordinates = this._dataTextureGenerator.generateTextureForPositions(gl, buffer.positionsCompressed, buffer.lenPositionsCompressed); textureState.texturePerLineIdPortionIds8Bits = this._dataTextureGenerator.generateTextureForPackedPortionIds(gl, buffer.perLineNumberPortionId8Bits); textureState.texturePerLineIdPortionIds16Bits = this._dataTextureGenerator.generateTextureForPackedPortionIds(gl, buffer.perLineNumberPortionId16Bits); textureState.texturePerLineIdPortionIds32Bits = this._dataTextureGenerator.generateTextureForPackedPortionIds(gl, buffer.perLineNumberPortionId32Bits); if (buffer.lenIndices8Bits > 0) { textureState.texturePerLineIdIndices8Bits = this._dataTextureGenerator.generateTextureFor8BitIndices(gl, buffer.indices8Bits, buffer.lenIndices8Bits); } if (buffer.lenIndices16Bits > 0) { textureState.texturePerLineIdIndices16Bits = this._dataTextureGenerator.generateTextureFor16BitIndices(gl, buffer.indices16Bits, buffer.lenIndices16Bits); } if (buffer.lenIndices32Bits > 0) { textureState.texturePerLineIdIndices32Bits = this._dataTextureGenerator.generateTextureFor32BitIndices(gl, buffer.indices32Bits, buffer.lenIndices32Bits); } textureState.finalize(); // Free up memory this._buffer = null; this._bucketGeometries = {}; this._finalized = true; this._deferredSetFlagsDirty = false; // this._onSceneRendering = this.model.scene.on("rendering", () => { if (this._deferredSetFlagsDirty) { this._uploadDeferredFlags(); } this._numUpdatesInFrame = 0; }); } initFlags(portionId, flags, meshTransparent) { if (flags & ENTITY_FLAGS.VISIBLE) { this._numVisibleLayerPortions++; this.model.numVisibleLayerPortions++; } if (flags & ENTITY_FLAGS.HIGHLIGHTED) { this._numHighlightedLayerPortions++; this.model.numHighlightedLayerPortions++; } if (flags & ENTITY_FLAGS.XRAYED) { this._numXRayedLayerPortions++; this.model.numXRayedLayerPortions++; } if (flags & ENTITY_FLAGS.SELECTED) { this._numSelectedLayerPortions++; this.model.numSelectedLayerPortions++; } if (flags & ENTITY_FLAGS.CLIPPABLE) { this._numClippableLayerPortions++; this.model.numClippableLayerPortions++; } if (flags & ENTITY_FLAGS.PICKABLE) { this._numPickableLayerPortions++; this.model.numPickableLayerPortions++; } if (flags & ENTITY_FLAGS.CULLED) { this._numCulledLayerPortions++; this.model.numCulledLayerPortions++; } if (meshTransparent) { this._numTransparentLayerPortions++; this.model.numTransparentLayerPortions++; } const deferred = true; this._setFlags(portionId, flags, meshTransparent, deferred); this._setFlags2(portionId, flags, deferred); } flushInitFlags() { this._setDeferredFlags(); this._setDeferredFlags2(); } setVisible(portionId, flags, transparent) { if (!this._finalized) { throw "Not finalized"; } if (flags & ENTITY_FLAGS.VISIBLE) { this._numVisibleLayerPortions++; this.model.numVisibleLayerPortions++; } else { this._numVisibleLayerPortions--; this.model.numVisibleLayerPortions--; } this._setFlags(portionId, flags, transparent); } setHighlighted(portionId, flags, transparent) { if (!this._finalized) { throw "Not finalized"; } if (flags & ENTITY_FLAGS.HIGHLIGHTED) { this._numHighlightedLayerPortions++; this.model.numHighlightedLayerPortions++; } else { this._numHighlightedLayerPortions--; this.model.numHighlightedLayerPortions--; } this._setFlags(portionId, flags, transparent); } setXRayed(portionId, flags, transparent) { if (!this._finalized) { throw "Not finalized"; } if (flags & ENTITY_FLAGS.XRAYED) { this._numXRayedLayerPortions++; this.model.numXRayedLayerPortions++; } else { this._numXRayedLayerPortions--; this.model.numXRayedLayerPortions--; } this._setFlags(portionId, flags, transparent); } setSelected(portionId, flags, transparent) { if (!this._finalized) { throw "Not finalized"; } if (flags & ENTITY_FLAGS.SELECTED) { this._numSelectedLayerPortions++; this.model.numSelectedLayerPortions++; } else { this._numSelectedLayerPortions--; this.model.numSelectedLayerPortions--; } this._setFlags(portionId, flags, transparent); } setEdges(portionId, flags, transparent) { } setClippable(portionId, flags) { if (!this._finalized) { throw "Not finalized"; } if (flags & ENTITY_FLAGS.CLIPPABLE) { this._numClippableLayerPortions++; this.model.numClippableLayerPortions++; } else { this._numClippableLayerPortions--; this.model.numClippableLayerPortions--; } this._setFlags2(portionId, flags); } /** * This will _start_ a "set-flags transaction". * * After invoking this method, calling setFlags/setFlags2 will not update * the colors+flags texture but only store the new flags/flag2 in the * colors+flags texture data array. * * After invoking this method, and when all desired setFlags/setFlags2 have * been called on needed portions of the layer, invoke `_uploadDeferredFlags` * to actually upload the data array into the texture. * * In massive "set-flags" scenarios like VFC or LOD mechanisms, the combination of * `_beginDeferredFlags` + `_uploadDeferredFlags`brings a speed-up of * up to 80x when e.g. objects are massively (un)culled 🚀. */ _beginDeferredFlags() { this._deferredSetFlagsActive = true; } /** * This will _commit_ a "set-flags transaction". * * Invoking this method will update the colors+flags texture data with new * flags/flags2 set since the previous invocation of `_beginDeferredFlags`. */ _uploadDeferredFlags() { this._deferredSetFlagsActive = false; if (!this._deferredSetFlagsDirty) { return; } this._deferredSetFlagsDirty = false; const gl = this.model.scene.canvas.gl; const textureState = this._dataTextureState; gl.bindTexture(gl.TEXTURE_2D, textureState.texturePerObjectColorsAndFlags._texture); gl.texSubImage2D( gl.TEXTURE_2D, 0, // level 0, // xoffset 0, // yoffset textureState.texturePerObjectColorsAndFlags._textureWidth, // width textureState.texturePerObjectColorsAndFlags._textureHeight, // width gl.RGBA_INTEGER, gl.UNSIGNED_BYTE, textureState.texturePerObjectColorsAndFlags._textureData ); } setCulled(portionId, flags, transparent) { if (!this._finalized) { throw "Not finalized"; } if (flags & ENTITY_FLAGS.CULLED) { this._numCulledLayerPortions += this._portionToSubPortionsMap[portionId].length; this.model.numCulledLayerPortions++; } else { this._numCulledLayerPortions -= this._portionToSubPortionsMap[portionId].length; this.model.numCulledLayerPortions--; } this._setFlags(portionId, flags, transparent); } setCollidable(portionId, flags) { if (!this._finalized) { throw "Not finalized"; } } setPickable(portionId, flags, transparent) { if (!this._finalized) { throw "Not finalized"; } if (flags & ENTITY_FLAGS.PICKABLE) { this._numPickableLayerPortions++; this.model.numPickableLayerPortions++; } else { this._numPickableLayerPortions--; this.model.numPickableLayerPortions--; } this._setFlags(portionId, flags, transparent); } setColor(portionId, color) { const subPortionIds = this._portionToSubPortionsMap[portionId]; for (let i = 0, len = subPortionIds.length; i < len; i++) { this._subPortionSetColor(subPortionIds[i], color); } } _subPortionSetColor(subPortionId, color) { if (!this._finalized) { throw "Not finalized"; } // Color const textureState = this._dataTextureState; const gl = this.model.scene.canvas.gl; tempUint8Array4$1 [0] = color[0]; tempUint8Array4$1 [1] = color[1]; tempUint8Array4$1 [2] = color[2]; tempUint8Array4$1 [3] = color[3]; // object colors textureState.texturePerObjectColorsAndFlags._textureData.set(tempUint8Array4$1, subPortionId * 32); if (this._deferredSetFlagsActive) { this._deferredSetFlagsDirty = true; return; } if (++this._numUpdatesInFrame >= MAX_OBJECT_UPDATES_IN_FRAME_WITHOUT_BATCHED_UPDATE$1) { this._beginDeferredFlags(); // Subsequent flags updates now deferred } gl.bindTexture(gl.TEXTURE_2D, textureState.texturePerObjectColorsAndFlags._texture); gl.texSubImage2D( gl.TEXTURE_2D, 0, // level (subPortionId % 512) * 8, // xoffset Math.floor(subPortionId / 512), // yoffset 1, // width 1, //height gl.RGBA_INTEGER, gl.UNSIGNED_BYTE, tempUint8Array4$1 ); // gl.bindTexture (gl.TEXTURE_2D, null); } setTransparent(portionId, flags, transparent) { if (transparent) { this._numTransparentLayerPortions++; this.model.numTransparentLayerPortions++; } else { this._numTransparentLayerPortions--; this.model.numTransparentLayerPortions--; } this._setFlags(portionId, flags, transparent); } _setFlags(portionId, flags, transparent, deferred = false) { const subPortionIds = this._portionToSubPortionsMap[portionId]; for (let i = 0, len = subPortionIds.length; i < len; i++) { this._subPortionSetFlags(subPortionIds[i], flags, transparent, deferred); } } _subPortionSetFlags(subPortionId, flags, transparent, deferred = false) { if (!this._finalized) { throw "Not finalized"; } const visible = !!(flags & ENTITY_FLAGS.VISIBLE); const xrayed = !!(flags & ENTITY_FLAGS.XRAYED); const highlighted = !!(flags & ENTITY_FLAGS.HIGHLIGHTED); const selected = !!(flags & ENTITY_FLAGS.SELECTED); const pickable = !!(flags & ENTITY_FLAGS.PICKABLE); const culled = !!(flags & ENTITY_FLAGS.CULLED); // Color let f0; if (!visible || culled || xrayed) { // Highlight & select are layered on top of color - not mutually exclusive f0 = RENDER_PASSES.NOT_RENDERED; } else { if (transparent) { f0 = RENDER_PASSES.COLOR_TRANSPARENT; } else { f0 = RENDER_PASSES.COLOR_OPAQUE; } } // Silhouette let f1; if (!visible || culled) { f1 = RENDER_PASSES.NOT_RENDERED; } else if (selected) { f1 = RENDER_PASSES.SILHOUETTE_SELECTED; } else if (highlighted) { f1 = RENDER_PASSES.SILHOUETTE_HIGHLIGHTED; } else if (xrayed) { f1 = RENDER_PASSES.SILHOUETTE_XRAYED; } else { f1 = RENDER_PASSES.NOT_RENDERED; } // // Edges // // let f2 = 0; // if (!visible || culled) { // f2 = RENDER_PASSES.NOT_RENDERED; // } else if (selected) { // f2 = RENDER_PASSES.EDGES_SELECTED; // } else if (highlighted) { // f2 = RENDER_PASSES.EDGES_HIGHLIGHTED; // } else if (xrayed) { // f2 = RENDER_PASSES.EDGES_XRAYED; // } else if (edges) { // if (transparent) { // f2 = RENDER_PASSES.EDGES_COLOR_TRANSPARENT; // } else { // f2 = RENDER_PASSES.EDGES_COLOR_OPAQUE; // } // } else { // f2 = RENDER_PASSES.NOT_RENDERED; // } // Pick let f3 = (visible && (!culled) && pickable) ? RENDER_PASSES.PICK : RENDER_PASSES.NOT_RENDERED; const textureState = this._dataTextureState; const gl = this.model.scene.canvas.gl; tempUint8Array4$1 [0] = f0; tempUint8Array4$1 [1] = f1; // tempUint8Array4 [2] = f2; tempUint8Array4$1 [3] = f3; // object flags textureState.texturePerObjectColorsAndFlags._textureData.set(tempUint8Array4$1, subPortionId * 32 + 8); if (this._deferredSetFlagsActive || deferred) { this._deferredSetFlagsDirty = true; return; } if (++this._numUpdatesInFrame >= MAX_OBJECT_UPDATES_IN_FRAME_WITHOUT_BATCHED_UPDATE$1) { this._beginDeferredFlags(); // Subsequent flags updates now deferred } gl.bindTexture(gl.TEXTURE_2D, textureState.texturePerObjectColorsAndFlags._texture); gl.texSubImage2D( gl.TEXTURE_2D, 0, // level (subPortionId % 512) * 8 + 2, // xoffset Math.floor(subPortionId / 512), // yoffset 1, // width 1, //height gl.RGBA_INTEGER, gl.UNSIGNED_BYTE, tempUint8Array4$1 ); // gl.bindTexture (gl.TEXTURE_2D, null); } _setDeferredFlags() { } _setFlags2(portionId, flags, deferred = false) { const subPortionIds = this._portionToSubPortionsMap[portionId]; for (let i = 0, len = subPortionIds.length; i < len; i++) { this._subPortionSetFlags2(subPortionIds[i], flags, deferred); } } _subPortionSetFlags2(subPortionId, flags, deferred = false) { if (!this._finalized) { throw "Not finalized"; } const clippable = !!(flags & ENTITY_FLAGS.CLIPPABLE) ? 255 : 0; const textureState = this._dataTextureState; const gl = this.model.scene.canvas.gl; tempUint8Array4$1 [0] = clippable; tempUint8Array4$1 [1] = 0; tempUint8Array4$1 [2] = 1; tempUint8Array4$1 [3] = 2; // object flags2 textureState.texturePerObjectColorsAndFlags._textureData.set(tempUint8Array4$1, subPortionId * 32 + 12); if (this._deferredSetFlagsActive || deferred) { // console.log("_subPortionSetFlags2 set flags defer"); this._deferredSetFlagsDirty = true; return; } if (++this._numUpdatesInFrame >= MAX_OBJECT_UPDATES_IN_FRAME_WITHOUT_BATCHED_UPDATE$1) { this._beginDeferredFlags(); // Subsequent flags updates now deferred } gl.bindTexture(gl.TEXTURE_2D, textureState.texturePerObjectColorsAndFlags._texture); gl.texSubImage2D( gl.TEXTURE_2D, 0, // level (subPortionId % 512) * 8 + 3, // xoffset Math.floor(subPortionId / 512), // yoffset 1, // width 1, //height gl.RGBA_INTEGER, gl.UNSIGNED_BYTE, tempUint8Array4$1 ); // gl.bindTexture (gl.TEXTURE_2D, null); } _setDeferredFlags2() { } setOffset(portionId, offset) { const subPortionIds = this._portionToSubPortionsMap[portionId]; for (let i = 0, len = subPortionIds.length; i < len; i++) { this._subPortionSetOffset(subPortionIds[i], offset); } } _subPortionSetOffset(subPortionId, offset) { if (!this._finalized) { throw "Not finalized"; } // if (!this.model.scene.entityOffsetsEnabled) { // this.model.error("Entity#offset not enabled for this Viewer"); // See Viewer entityOffsetsEnabled // return; // } const textureState = this._dataTextureState; const gl = this.model.scene.canvas.gl; tempFloat32Array3$1 [0] = offset[0]; tempFloat32Array3$1 [1] = offset[1]; tempFloat32Array3$1 [2] = offset[2]; // object offset textureState.texturePerObjectOffsets._textureData.set(tempFloat32Array3$1, subPortionId * 3); if (this._deferredSetFlagsActive) { this._deferredSetFlagsDirty = true; return; } if (++this._numUpdatesInFrame >= MAX_OBJECT_UPDATES_IN_FRAME_WITHOUT_BATCHED_UPDATE$1) { this._beginDeferredFlags(); // Subsequent flags updates now deferred } gl.bindTexture(gl.TEXTURE_2D, textureState.texturePerObjectOffsets._texture); gl.texSubImage2D( gl.TEXTURE_2D, 0, // level 0, // x offset subPortionId, // yoffset 1, // width 1, // height gl.RGB, gl.FLOAT, tempFloat32Array3$1 ); // gl.bindTexture (gl.TEXTURE_2D, null); } setMatrix(portionId, matrix) { const subPortionIds = this._portionToSubPortionsMap[portionId]; for (let i = 0, len = subPortionIds.length; i < len; i++) { this._subPortionSetMatrix(subPortionIds[i], matrix); } } _subPortionSetMatrix(subPortionId, matrix) { if (!this._finalized) { throw "Not finalized"; } // if (!this.model.scene.entityMatrixsEnabled) { // this.model.error("Entity#matrix not enabled for this Viewer"); // See Viewer entityMatrixsEnabled // return; // } const textureState = this._dataTextureState; const gl = this.model.scene.canvas.gl; tempMat4a$d.set(matrix); textureState.texturePerObjectInstanceMatrices._textureData.set(tempMat4a$d, subPortionId * 16); if (this._deferredSetFlagsActive) { this._deferredSetFlagsDirty = true; return; } if (++this._numUpdatesInFrame >= MAX_OBJECT_UPDATES_IN_FRAME_WITHOUT_BATCHED_UPDATE$1) { this._beginDeferredFlags(); // Subsequent flags updates now deferred } gl.bindTexture(gl.TEXTURE_2D, textureState.texturePerObjectInstanceMatrices._texture); gl.texSubImage2D( gl.TEXTURE_2D, 0, // level (subPortionId % 512) * 4, // xoffset Math.floor(subPortionId / 512), // yoffset // 1, 4, // width 1, // height gl.RGBA, gl.FLOAT, tempMat4a$d ); // gl.bindTexture (gl.TEXTURE_2D, null); } // ---------------------- COLOR RENDERING ----------------------------------- drawColorOpaque(renderFlags, frameCtx) { if (this._numCulledLayerPortions === this._numPortions || this._numVisibleLayerPortions === 0 || this._numTransparentLayerPortions === this._numPortions || this._numXRayedLayerPortions === this._numPortions) { return; } if (this._renderers.colorRenderer) { this._renderers.colorRenderer.drawLayer(frameCtx, this, RENDER_PASSES.COLOR_OPAQUE); } } drawColorTransparent(renderFlags, frameCtx) { if (this._numCulledLayerPortions === this._numPortions || this._numVisibleLayerPortions === 0 || this._numTransparentLayerPortions === 0 || this._numXRayedLayerPortions === this._numPortions) { return; } if (this._renderers.colorRenderer) { this._renderers.colorRenderer.drawLayer(frameCtx, this, RENDER_PASSES.COLOR_TRANSPARENT); } } drawDepth(renderFlags, frameCtx) { } drawNormals(renderFlags, frameCtx) { } drawSilhouetteXRayed(renderFlags, frameCtx) { // if (this._numCulledLayerPortions === this._numPortions || this._numVisibleLayerPortions === 0 || this._numXRayedLayerPortions === 0) { // return; // } // if (this._renderers.silhouetteRenderer) { // this._renderers.silhouetteRenderer.drawLayer(frameCtx, this, RENDER_PASSES.SILHOUETTE_XRAYED); // } } drawSilhouetteHighlighted(renderFlags, frameCtx) { // if (this._numCulledLayerPortions === this._numPortions || this._numVisibleLayerPortions === 0 || this._numHighlightedLayerPortions === 0) { // return; // } // if (this._renderers.silhouetteRenderer) { // this._renderers.silhouetteRenderer.drawLayer(frameCtx, this, RENDER_PASSES.SILHOUETTE_HIGHLIGHTED); // } } drawSilhouetteSelected(renderFlags, frameCtx) { // if (this._numCulledLayerPortions === this._numPortions || this._numVisibleLayerPortions === 0 || this._numSelectedLayerPortions === 0) { // return; // } // if (this._renderers.silhouetteRenderer) { // this._renderers.silhouetteRenderer.drawLayer(frameCtx, this, RENDER_PASSES.SILHOUETTE_SELECTED); // } } drawEdgesColorOpaque(renderFlags, frameCtx) { } drawEdgesColorTransparent(renderFlags, frameCtx) { } drawEdgesHighlighted(renderFlags, frameCtx) { } drawEdgesSelected(renderFlags, frameCtx) { } drawEdgesXRayed(renderFlags, frameCtx) { } drawOcclusion(renderFlags, frameCtx) { } drawShadow(renderFlags, frameCtx) { } setPickMatrices(pickViewMatrix, pickProjMatrix) { } drawPickMesh(renderFlags, frameCtx) { } drawPickDepths(renderFlags, frameCtx) { } drawSnapInit(renderFlags, frameCtx) { } drawSnap(renderFlags, frameCtx) { } drawPickNormals(renderFlags, frameCtx) { } destroy() { if (this._destroyed) { return; } const state = this._state; this.model.scene.off(this._onSceneRendering); state.destroy(); this._destroyed = true; } } const tempVec3a$p = math.vec3(); const tempVec3b$l = math.vec3(); const tempVec3c$h = math.vec3(); math.vec3(); const tempVec4a$b = math.vec4(); const tempMat4a$c = math.mat4(); /** * @private */ class DTXTrianglesColorRenderer { constructor(scene, withSAO) { this._scene = scene; this._withSAO = withSAO; this._hash = this._getHash(); this._allocate(); } getValid() { return this._hash === this._getHash(); }; _getHash() { const scene = this._scene; return [scene._lightsState.getHash(), scene._sectionPlanesState.getHash(), (this._withSAO ? "sao" : "nosao")].join(";"); } drawLayer(frameCtx, dataTextureLayer, renderPass) { const scene = this._scene; const camera = scene.camera; const model = dataTextureLayer.model; const gl = scene.canvas.gl; const state = dataTextureLayer._state; const textureState = state.textureState; const origin = dataTextureLayer._state.origin; const {position, rotationMatrix} = model; if (!this._program) { this._allocate(); if (this.errors) { return; } } if (frameCtx.lastProgramId !== this._program.id) { frameCtx.lastProgramId = this._program.id; this._bindProgram(frameCtx, state); } textureState.bindCommonTextures( this._program, this.uTexturePerObjectPositionsDecodeMatrix, this._uTexturePerVertexIdCoordinates, this.uTexturePerObjectColorsAndFlags, this._uTexturePerObjectMatrix ); let rtcViewMatrix; let rtcCameraEye; const gotOrigin = (origin[0] !== 0 || origin[1] !== 0 || origin[2] !== 0); const gotPosition = (position[0] !== 0 || position[1] !== 0 || position[2] !== 0); if (gotOrigin || gotPosition) { const rtcOrigin = tempVec3a$p; if (gotOrigin) { const rotatedOrigin = math.transformPoint3(rotationMatrix, origin, tempVec3b$l); rtcOrigin[0] = rotatedOrigin[0]; rtcOrigin[1] = rotatedOrigin[1]; rtcOrigin[2] = rotatedOrigin[2]; } else { rtcOrigin[0] = 0; rtcOrigin[1] = 0; rtcOrigin[2] = 0; } rtcOrigin[0] += position[0]; rtcOrigin[1] += position[1]; rtcOrigin[2] += position[2]; rtcViewMatrix = createRTCViewMat(camera.viewMatrix, rtcOrigin, tempMat4a$c); rtcCameraEye = tempVec3c$h; rtcCameraEye[0] = camera.eye[0] - rtcOrigin[0]; rtcCameraEye[1] = camera.eye[1] - rtcOrigin[1]; rtcCameraEye[2] = camera.eye[2] - rtcOrigin[2]; } else { rtcViewMatrix = camera.viewMatrix; rtcCameraEye = camera.eye; } gl.uniformMatrix4fv(this._uSceneModelMatrix, false, rotationMatrix); gl.uniformMatrix4fv(this._uViewMatrix, false, rtcViewMatrix); gl.uniformMatrix4fv(this._uProjMatrix, false, camera.projMatrix); gl.uniform3fv(this._uCameraEyeRtc, rtcCameraEye); gl.uniform1i(this._uRenderPass, renderPass); if (scene.logarithmicDepthBufferEnabled) { const logDepthBufFC = 2.0 / (Math.log(frameCtx.pickZFar + 1.0) / Math.LN2); gl.uniform1f(this._uLogDepthBufFC, logDepthBufFC); } const numAllocatedSectionPlanes = scene._sectionPlanesState.getNumAllocatedSectionPlanes(); const numSectionPlanes = scene._sectionPlanesState.sectionPlanes.length; if (numAllocatedSectionPlanes > 0) { const sectionPlanes = scene._sectionPlanesState.sectionPlanes; const baseIndex = dataTextureLayer.layerIndex * numSectionPlanes; const renderFlags = model.renderFlags; if (scene.crossSections) { gl.uniform4fv(this._uSliceColor, scene.crossSections.sliceColor); gl.uniform1f(this._uSliceThickness, scene.crossSections.sliceThickness); } for (let sectionPlaneIndex = 0; sectionPlaneIndex < numAllocatedSectionPlanes; sectionPlaneIndex++) { const sectionPlaneUniforms = this._uSectionPlanes[sectionPlaneIndex]; if (sectionPlaneUniforms) { if (sectionPlaneIndex < numSectionPlanes) { const active = renderFlags.sectionPlanesActivePerLayer[baseIndex + sectionPlaneIndex]; gl.uniform1i(sectionPlaneUniforms.active, active ? 1 : 0); if (active) { const sectionPlane = sectionPlanes[sectionPlaneIndex]; if (origin) { const rtcSectionPlanePos = getPlaneRTCPos(sectionPlane.dist, sectionPlane.dir, origin, tempVec3a$p, model.matrix); gl.uniform3fv(sectionPlaneUniforms.pos, rtcSectionPlanePos); } else { gl.uniform3fv(sectionPlaneUniforms.pos, sectionPlane.pos); } gl.uniform3fv(sectionPlaneUniforms.dir, sectionPlane.dir); } } else { gl.uniform1i(sectionPlaneUniforms.active, 0); } } } } if (state.numIndices8Bits > 0) { textureState.bindTriangleIndicesTextures( this._program, this._uTexturePerPolygonIdPortionIds, this._uTexturePerPolygonIdIndices, 8 // 8 bits indices ); gl.drawArrays(gl.TRIANGLES, 0, state.numIndices8Bits); } if (state.numIndices16Bits > 0) { textureState.bindTriangleIndicesTextures( this._program, this._uTexturePerPolygonIdPortionIds, this._uTexturePerPolygonIdIndices, 16 // 16 bits indices ); gl.drawArrays(gl.TRIANGLES, 0, state.numIndices16Bits); } if (state.numIndices32Bits > 0) { textureState.bindTriangleIndicesTextures( this._program, this._uTexturePerPolygonIdPortionIds, this._uTexturePerPolygonIdIndices, 32 // 32 bits indices ); gl.drawArrays(gl.TRIANGLES, 0, state.numIndices32Bits); } frameCtx.drawElements++; } _allocate() { const scene = this._scene; const gl = scene.canvas.gl; const lightsState = scene._lightsState; this._program = new Program(gl, this._buildShader()); if (this._program.errors) { this.errors = this._program.errors; console.error(this.errors); return; } const program = this._program; this._uRenderPass = program.getLocation("renderPass"); this._uLightAmbient = program.getLocation("lightAmbient"); this._uLightColor = []; this._uLightDir = []; this._uLightPos = []; this._uLightAttenuation = []; const lights = lightsState.lights; let light; for (let i = 0, len = lights.length; i < len; i++) { light = lights[i]; switch (light.type) { case "dir": this._uLightColor[i] = program.getLocation("lightColor" + i); this._uLightPos[i] = null; this._uLightDir[i] = program.getLocation("lightDir" + i); break; case "point": this._uLightColor[i] = program.getLocation("lightColor" + i); this._uLightPos[i] = program.getLocation("lightPos" + i); this._uLightDir[i] = null; this._uLightAttenuation[i] = program.getLocation("lightAttenuation" + i); break; case "spot": this._uLightColor[i] = program.getLocation("lightColor" + i); this._uLightPos[i] = program.getLocation("lightPos" + i); this._uLightDir[i] = program.getLocation("lightDir" + i); this._uLightAttenuation[i] = program.getLocation("lightAttenuation" + i); break; } } this._uSceneModelMatrix = program.getLocation("sceneModelMatrix"); this._uViewMatrix = program.getLocation("viewMatrix"); this._uProjMatrix = program.getLocation("projMatrix"); this._uSectionPlanes = []; for (let i = 0, len = scene._sectionPlanesState.getNumAllocatedSectionPlanes(); i < len; i++) { this._uSectionPlanes.push({ active: program.getLocation("sectionPlaneActive" + i), pos: program.getLocation("sectionPlanePos" + i), dir: program.getLocation("sectionPlaneDir" + i) }); } if (this._withSAO) { this._uOcclusionTexture = "uOcclusionTexture"; this._uSAOParams = program.getLocation("uSAOParams"); } if (scene.logarithmicDepthBufferEnabled) { this._uLogDepthBufFC = program.getLocation("logDepthBufFC"); } this.uTexturePerObjectPositionsDecodeMatrix = "uObjectPerObjectPositionsDecodeMatrix"; this.uTexturePerObjectColorsAndFlags = "uObjectPerObjectColorsAndFlags"; this._uTexturePerVertexIdCoordinates = "uTexturePerVertexIdCoordinates"; this._uTexturePerPolygonIdNormals = "uTexturePerPolygonIdNormals"; this._uTexturePerPolygonIdIndices = "uTexturePerPolygonIdIndices"; this._uTexturePerPolygonIdPortionIds = "uTexturePerPolygonIdPortionIds"; this._uTexturePerObjectMatrix= "uTexturePerObjectMatrix"; this._uCameraEyeRtc = program.getLocation("uCameraEyeRtc"); if (scene.crossSections) { this._uSliceColor = program.getLocation("sliceColor"); this._uSliceThickness = program.getLocation("sliceThickness"); } } _bindProgram(frameCtx) { const scene = this._scene; const gl = scene.canvas.gl; const program = this._program; const lights = scene._lightsState.lights; const project = scene.camera.project; program.bind(); if (this._uLightAmbient) { gl.uniform4fv(this._uLightAmbient, scene._lightsState.getAmbientColorAndIntensity()); } for (let i = 0, len = lights.length; i < len; i++) { const light = lights[i]; if (this._uLightColor[i]) { gl.uniform4f(this._uLightColor[i], light.color[0], light.color[1], light.color[2], light.intensity); } if (this._uLightPos[i]) { gl.uniform3fv(this._uLightPos[i], light.pos); if (this._uLightAttenuation[i]) { gl.uniform1f(this._uLightAttenuation[i], light.attenuation); } } if (this._uLightDir[i]) { gl.uniform3fv(this._uLightDir[i], light.dir); } } if (this._withSAO) { const sao = scene.sao; const saoEnabled = sao.possible; if (saoEnabled) { const viewportWidth = gl.drawingBufferWidth; const viewportHeight = gl.drawingBufferHeight; tempVec4a$b[0] = viewportWidth; tempVec4a$b[1] = viewportHeight; tempVec4a$b[2] = sao.blendCutoff; tempVec4a$b[3] = sao.blendFactor; gl.uniform4fv(this._uSAOParams, tempVec4a$b); this._program.bindTexture(this._uOcclusionTexture, frameCtx.occlusionTexture, 10); } } if (scene.logarithmicDepthBufferEnabled) { const logDepthBufFC = 2.0 / (Math.log(project.far + 1.0) / Math.LN2); gl.uniform1f(this._uLogDepthBufFC, logDepthBufFC); } } _buildShader() { return { vertex: this._buildVertexShader(), fragment: this._buildFragmentShader() }; } _buildVertexShader() { const scene = this._scene; const sectionPlanesState = scene._sectionPlanesState; const lightsState = scene._lightsState; const clipping = sectionPlanesState.getNumAllocatedSectionPlanes() > 0; let light; const src = []; src.push("#version 300 es"); src.push("// TrianglesDataTextureColorRenderer vertex shader"); src.push("#ifdef GL_FRAGMENT_PRECISION_HIGH"); src.push("precision highp float;"); src.push("precision highp int;"); src.push("precision highp usampler2D;"); src.push("precision highp isampler2D;"); src.push("precision highp sampler2D;"); src.push("#else"); src.push("precision mediump float;"); src.push("precision mediump int;"); src.push("precision mediump usampler2D;"); src.push("precision mediump isampler2D;"); src.push("precision mediump sampler2D;"); src.push("#endif"); src.push("uniform int renderPass;"); src.push("uniform mat4 sceneModelMatrix;"); src.push("uniform mat4 viewMatrix;"); src.push("uniform mat4 projMatrix;"); src.push("uniform highp sampler2D uObjectPerObjectPositionsDecodeMatrix;"); src.push("uniform highp sampler2D uTexturePerObjectMatrix;"); src.push("uniform lowp usampler2D uObjectPerObjectColorsAndFlags;"); src.push("uniform mediump usampler2D uTexturePerVertexIdCoordinates;"); src.push("uniform highp usampler2D uTexturePerPolygonIdIndices;"); src.push("uniform mediump usampler2D uTexturePerPolygonIdPortionIds;"); src.push("uniform vec3 uCameraEyeRtc;"); src.push("vec3 positions[3];"); if (scene.logarithmicDepthBufferEnabled) { src.push("uniform float logDepthBufFC;"); src.push("out float vFragDepth;"); src.push("out float isPerspective;"); } src.push("bool isPerspectiveMatrix(mat4 m) {"); src.push(" return (m[2][3] == - 1.0);"); src.push("}"); src.push("uniform vec4 lightAmbient;"); for (let i = 0, len = lightsState.lights.length; i < len; i++) { light = lightsState.lights[i]; if (light.type === "ambient") { continue; } src.push("uniform vec4 lightColor" + i + ";"); if (light.type === "dir") { src.push("uniform vec3 lightDir" + i + ";"); } if (light.type === "point") { src.push("uniform vec3 lightPos" + i + ";"); } if (light.type === "spot") { src.push("uniform vec3 lightPos" + i + ";"); src.push("uniform vec3 lightDir" + i + ";"); } } if (clipping) { src.push("out vec4 vWorldPosition;"); src.push("flat out uint vFlags2;"); } src.push("out vec4 vColor;"); src.push("void main(void) {"); // constants src.push("int polygonIndex = gl_VertexID / 3;"); // get packed object-id src.push("int h_packed_object_id_index = (polygonIndex >> 3) & 4095;"); src.push("int v_packed_object_id_index = (polygonIndex >> 3) >> 12;"); src.push("int objectIndex = int(texelFetch(uTexturePerPolygonIdPortionIds, ivec2(h_packed_object_id_index, v_packed_object_id_index), 0).r);"); src.push("ivec2 objectIndexCoords = ivec2(objectIndex % 512, objectIndex / 512);"); // get flags & flags2 src.push("uvec4 flags = texelFetch (uObjectPerObjectColorsAndFlags, ivec2(objectIndexCoords.x*8+2, objectIndexCoords.y), 0);"); src.push("uvec4 flags2 = texelFetch (uObjectPerObjectColorsAndFlags, ivec2(objectIndexCoords.x*8+3, objectIndexCoords.y), 0);"); // flags.x = NOT_RENDERED | COLOR_OPAQUE | COLOR_TRANSPARENT // renderPass = COLOR_OPAQUE src.push(`if (int(flags.x) != renderPass) {`); src.push(" gl_Position = vec4(3.0, 3.0, 3.0, 1.0);"); // Cull vertex src.push(" return;"); // Cull vertex src.push("} else {"); src.push("ivec4 packedVertexBase = ivec4(texelFetch (uObjectPerObjectColorsAndFlags, ivec2(objectIndexCoords.x*8+4, objectIndexCoords.y), 0));"); src.push("ivec4 packedIndexBaseOffset = ivec4(texelFetch (uObjectPerObjectColorsAndFlags, ivec2(objectIndexCoords.x*8+5, objectIndexCoords.y), 0));"); src.push("int indexBaseOffset = (packedIndexBaseOffset.r << 24) + (packedIndexBaseOffset.g << 16) + (packedIndexBaseOffset.b << 8) + packedIndexBaseOffset.a;"); src.push("int h_index = (polygonIndex - indexBaseOffset) & 4095;"); src.push("int v_index = (polygonIndex - indexBaseOffset) >> 12;"); src.push("ivec3 vertexIndices = ivec3(texelFetch(uTexturePerPolygonIdIndices, ivec2(h_index, v_index), 0));"); src.push("ivec3 uniqueVertexIndexes = vertexIndices + (packedVertexBase.r << 24) + (packedVertexBase.g << 16) + (packedVertexBase.b << 8) + packedVertexBase.a;"); src.push("ivec3 indexPositionH = uniqueVertexIndexes & 4095;"); src.push("ivec3 indexPositionV = uniqueVertexIndexes >> 12;"); src.push("mat4 objectInstanceMatrix = mat4 (texelFetch (uTexturePerObjectMatrix, ivec2(objectIndexCoords.x*4+0, objectIndexCoords.y), 0), texelFetch (uTexturePerObjectMatrix, ivec2(objectIndexCoords.x*4+1, objectIndexCoords.y), 0), texelFetch (uTexturePerObjectMatrix, ivec2(objectIndexCoords.x*4+2, objectIndexCoords.y), 0), texelFetch (uTexturePerObjectMatrix, ivec2(objectIndexCoords.x*4+3, objectIndexCoords.y), 0));"); src.push("mat4 objectDecodeAndInstanceMatrix = objectInstanceMatrix * mat4 (texelFetch (uObjectPerObjectPositionsDecodeMatrix, ivec2(objectIndexCoords.x*4+0, objectIndexCoords.y), 0), texelFetch (uObjectPerObjectPositionsDecodeMatrix, ivec2(objectIndexCoords.x*4+1, objectIndexCoords.y), 0), texelFetch (uObjectPerObjectPositionsDecodeMatrix, ivec2(objectIndexCoords.x*4+2, objectIndexCoords.y), 0), texelFetch (uObjectPerObjectPositionsDecodeMatrix, ivec2(objectIndexCoords.x*4+3, objectIndexCoords.y), 0));"); src.push("uint solid = texelFetch (uObjectPerObjectColorsAndFlags, ivec2(objectIndexCoords.x*8+7, objectIndexCoords.y), 0).r;"); src.push("positions[0] = vec3(texelFetch(uTexturePerVertexIdCoordinates, ivec2(indexPositionH.r, indexPositionV.r), 0));"); src.push("positions[1] = vec3(texelFetch(uTexturePerVertexIdCoordinates, ivec2(indexPositionH.g, indexPositionV.g), 0));"); src.push("positions[2] = vec3(texelFetch(uTexturePerVertexIdCoordinates, ivec2(indexPositionH.b, indexPositionV.b), 0));"); src.push("uvec4 color = texelFetch (uObjectPerObjectColorsAndFlags, ivec2(objectIndexCoords.x*8+0, objectIndexCoords.y), 0);"); src.push(`if (color.a == 0u) {`); src.push(" gl_Position = vec4(3.0, 3.0, 3.0, 1.0);"); // Cull vertex src.push(" return;"); src.push("};"); src.push("vec3 normal = normalize(cross(positions[2] - positions[0], positions[1] - positions[0]));"); src.push("vec3 position;"); src.push("position = positions[gl_VertexID % 3];"); src.push("vec3 viewNormal = -normalize((transpose(inverse(viewMatrix*objectDecodeAndInstanceMatrix)) * vec4(normal,1)).xyz);"); // when the geometry is not solid, if needed, flip the triangle winding src.push("if (solid != 1u) {"); src.push("if (isPerspectiveMatrix(projMatrix)) {"); src.push("vec3 uCameraEyeRtcInQuantizedSpace = (inverse(sceneModelMatrix * objectDecodeAndInstanceMatrix) * vec4(uCameraEyeRtc, 1)).xyz;"); // src.push("vColor = vec4(vec3(1, -1, 0)*dot(normalize(position.xyz - uCameraEyeRtcInQuantizedSpace), normal), 1);") src.push("if (dot(position.xyz - uCameraEyeRtcInQuantizedSpace, normal) < 0.0) {"); src.push("position = positions[2 - (gl_VertexID % 3)];"); src.push("viewNormal = -viewNormal;"); src.push("}"); src.push("} else {"); // src.push("vColor = vec4(vec3(1, -1, 0)*viewNormal.z, 1);") src.push("if (viewNormal.z < 0.0) {"); src.push("position = positions[2 - (gl_VertexID % 3)];"); src.push("viewNormal = -viewNormal;"); src.push("}"); src.push("}"); src.push("}"); src.push("vec4 worldPosition = sceneModelMatrix * ((objectDecodeAndInstanceMatrix * vec4(position, 1.0))); "); src.push("vec4 viewPosition = viewMatrix * worldPosition; "); src.push("vec3 reflectedColor = vec3(0.0, 0.0, 0.0);"); src.push("vec3 viewLightDir = vec3(0.0, 0.0, -1.0);"); src.push("float lambertian = 1.0;"); for (let i = 0, len = lightsState.lights.length; i < len; i++) { light = lightsState.lights[i]; if (light.type === "ambient") { continue; } if (light.type === "dir") { if (light.space === "view") { src.push("viewLightDir = normalize(lightDir" + i + ");"); } else { src.push("viewLightDir = normalize((viewMatrix * vec4(lightDir" + i + ", 0.0)).xyz);"); } } else if (light.type === "point") { if (light.space === "view") { src.push("viewLightDir = -normalize(lightPos" + i + " - viewPosition.xyz);"); } else { src.push("viewLightDir = -normalize((viewMatrix * vec4(lightPos" + i + ", 0.0)).xyz);"); } } else if (light.type === "spot") { if (light.space === "view") { src.push("viewLightDir = normalize(lightDir" + i + ");"); } else { src.push("viewLightDir = normalize((viewMatrix * vec4(lightDir" + i + ", 0.0)).xyz);"); } } else { continue; } src.push("lambertian = max(dot(-viewNormal, viewLightDir), 0.0);"); src.push("reflectedColor += lambertian * (lightColor" + i + ".rgb * lightColor" + i + ".a);"); } src.push("vec3 rgb = vec3(color.rgb) / 255.0;"); src.push("vColor = vec4((lightAmbient.rgb * lightAmbient.a * rgb) + (reflectedColor * rgb), float(color.a) / 255.0);"); src.push("vec4 clipPos = projMatrix * viewPosition;"); if (scene.logarithmicDepthBufferEnabled) { src.push("vFragDepth = 1.0 + clipPos.w;"); src.push("isPerspective = float (isPerspectiveMatrix(projMatrix));"); } if (clipping) { src.push("vWorldPosition = worldPosition;"); src.push("vFlags2 = flags2.r;"); } src.push("gl_Position = clipPos;"); src.push("}"); src.push("}"); return src; } _buildFragmentShader() { const scene = this._scene; const sectionPlanesState = scene._sectionPlanesState; const clipping = sectionPlanesState.getNumAllocatedSectionPlanes() > 0; const src = []; src.push('#version 300 es'); src.push("// TrianglesDataTextureColorRenderer fragment shader"); src.push("#ifdef GL_FRAGMENT_PRECISION_HIGH"); src.push("precision highp float;"); src.push("precision highp int;"); src.push("#else"); src.push("precision mediump float;"); src.push("precision mediump int;"); src.push("#endif"); if (scene.logarithmicDepthBufferEnabled) { src.push("in float isPerspective;"); src.push("uniform float logDepthBufFC;"); src.push("in float vFragDepth;"); } if (this._withSAO) { src.push("uniform sampler2D uOcclusionTexture;"); src.push("uniform vec4 uSAOParams;"); src.push("const float packUpscale = 256. / 255.;"); src.push("const float unpackDownScale = 255. / 256.;"); src.push("const vec3 packFactors = vec3( 256. * 256. * 256., 256. * 256., 256. );"); src.push("const vec4 unPackFactors = unpackDownScale / vec4( packFactors, 1. );"); src.push("float unpackRGBToFloat( const in vec4 v ) {"); src.push(" return dot( v, unPackFactors );"); src.push("}"); } if (clipping) { src.push("in vec4 vWorldPosition;"); src.push("flat in uint vFlags2;"); for (let i = 0, len = sectionPlanesState.getNumAllocatedSectionPlanes(); i < len; i++) { src.push("uniform bool sectionPlaneActive" + i + ";"); src.push("uniform vec3 sectionPlanePos" + i + ";"); src.push("uniform vec3 sectionPlaneDir" + i + ";"); } src.push("uniform float sliceThickness;"); src.push("uniform vec4 sliceColor;"); } src.push("in vec4 vColor;"); src.push("out vec4 outColor;"); src.push("void main(void) {"); src.push(" vec4 newColor;"); src.push(" newColor = vColor;"); if (clipping) { src.push(" bool clippable = vFlags2 > 0u;"); src.push(" if (clippable) {"); src.push(" float dist = 0.0;"); for (let i = 0, len = sectionPlanesState.getNumAllocatedSectionPlanes(); i < len; i++) { src.push("if (sectionPlaneActive" + i + ") {"); src.push(" dist += clamp(dot(-sectionPlaneDir" + i + ".xyz, vWorldPosition.xyz - sectionPlanePos" + i + ".xyz), 0.0, 1000.0);"); src.push("}"); } src.push(" if (dist > sliceThickness) { "); src.push(" discard;"); src.push(" }"); src.push(" if (dist > 0.0) { "); src.push(" newColor = sliceColor;"); src.push(" }"); src.push("}"); } if (scene.logarithmicDepthBufferEnabled) { src.push(" gl_FragDepth = isPerspective == 0.0 ? gl_FragCoord.z : log2( vFragDepth ) * logDepthBufFC * 0.5;"); //src.push(" gl_FragDepth = log2( vFragDepth ) * logDepthBufFC * 0.5;"); } else { src.push(" float dx = dFdx(gl_FragCoord.z);"); src.push(" float dy = dFdy(gl_FragCoord.z);"); src.push(" float diff = sqrt(dx*dx+dy*dy);"); src.push(" gl_FragDepth = gl_FragCoord.z + diff;"); } if (this._withSAO) { // Doing SAO blend in the main solid fill draw shader just so that edge lines can be drawn over the top // Would be more efficient to defer this, then render lines later, using same depth buffer for Z-reject src.push(" float viewportWidth = uSAOParams[0];"); src.push(" float viewportHeight = uSAOParams[1];"); src.push(" float blendCutoff = uSAOParams[2];"); src.push(" float blendFactor = uSAOParams[3];"); src.push(" vec2 uv = vec2(gl_FragCoord.x / viewportWidth, gl_FragCoord.y / viewportHeight);"); src.push(" float ambient = smoothstep(blendCutoff, 1.0, unpackRGBToFloat(texture(uOcclusionTexture, uv))) * blendFactor;"); src.push(" outColor = vec4(newColor.rgb * ambient, 1.0);"); } else { src.push(" outColor = newColor;"); } src.push("}"); return src; } webglContextRestored() { this._program = null; } destroy() { if (this._program) { this._program.destroy(); } this._program = null; } } const defaultColor$1 = new Float32Array([1, 1, 1]); const tempVec3a$o = math.vec3(); const tempVec3b$k = math.vec3(); const tempVec3c$g = math.vec3(); math.vec3(); const tempMat4a$b = math.mat4(); /** * @private */ class DTXTrianglesSilhouetteRenderer { constructor(scene, primitiveType) { this._scene = scene; this._hash = this._getHash(); this._allocate(); } getValid() { return this._hash === this._getHash(); }; _getHash() { return this._scene._sectionPlanesState.getHash(); } drawLayer(frameCtx, dataTextureLayer, renderPass) { const scene = this._scene; const camera = scene.camera; const model = dataTextureLayer.model; const gl = scene.canvas.gl; const state = dataTextureLayer._state; const textureState = state.textureState; const origin = dataTextureLayer._state.origin; const {position, rotationMatrix} = model; const viewMatrix = camera.viewMatrix; if (!this._program) { this._allocate(); if (this.errors) { return; } } if (frameCtx.lastProgramId !== this._program.id) { frameCtx.lastProgramId = this._program.id; this._bindProgram(frameCtx, state); } textureState.bindCommonTextures( this._program, this.uTexturePerObjectPositionsDecodeMatrix, this._uTexturePerVertexIdCoordinates, this.uTexturePerObjectColorsAndFlags, this._uTexturePerObjectMatrix ); let rtcViewMatrix; let rtcCameraEye; if (origin || position[0] !== 0 || position[1] !== 0 || position[2] !== 0) { const rtcOrigin = tempVec3a$o; if (origin) { const rotatedOrigin = tempVec3b$k; math.transformPoint3(rotationMatrix, origin, rotatedOrigin); rtcOrigin[0] = rotatedOrigin[0]; rtcOrigin[1] = rotatedOrigin[1]; rtcOrigin[2] = rotatedOrigin[2]; } else { rtcOrigin[0] = 0; rtcOrigin[1] = 0; rtcOrigin[2] = 0; } rtcOrigin[0] += position[0]; rtcOrigin[1] += position[1]; rtcOrigin[2] += position[2]; rtcViewMatrix = createRTCViewMat(viewMatrix, rtcOrigin, tempMat4a$b); rtcCameraEye = tempVec3c$g; rtcCameraEye[0] = camera.eye[0] - rtcOrigin[0]; rtcCameraEye[1] = camera.eye[1] - rtcOrigin[1]; rtcCameraEye[2] = camera.eye[2] - rtcOrigin[2]; } else { rtcViewMatrix = viewMatrix; rtcCameraEye = camera.eye; } gl.uniform3fv(this._uCameraEyeRtc, rtcCameraEye); gl.uniform1i(this._uRenderPass, renderPass); gl.uniformMatrix4fv(this._uWorldMatrix, false, rotationMatrix); gl.uniformMatrix4fv(this._uViewMatrix, false, rtcViewMatrix); gl.uniformMatrix4fv(this._uProjMatrix, false, camera.projMatrix); if (renderPass === RENDER_PASSES.SILHOUETTE_XRAYED) { const material = scene.xrayMaterial._state; const fillColor = material.fillColor; const fillAlpha = material.fillAlpha; gl.uniform4f(this._uColor, fillColor[0], fillColor[1], fillColor[2], fillAlpha); } else if (renderPass === RENDER_PASSES.SILHOUETTE_HIGHLIGHTED) { const material = scene.highlightMaterial._state; const fillColor = material.fillColor; const fillAlpha = material.fillAlpha; gl.uniform4f(this._uColor, fillColor[0], fillColor[1], fillColor[2], fillAlpha); } else if (renderPass === RENDER_PASSES.SILHOUETTE_SELECTED) { const material = scene.selectedMaterial._state; const fillColor = material.fillColor; const fillAlpha = material.fillAlpha; gl.uniform4f(this._uColor, fillColor[0], fillColor[1], fillColor[2], fillAlpha); } else { gl.uniform4fv(this._uColor, defaultColor$1); } if (scene.logarithmicDepthBufferEnabled) { const logDepthBufFC = 2.0 / (Math.log(frameCtx.pickZFar + 1.0) / Math.LN2); gl.uniform1f(this._uLogDepthBufFC, logDepthBufFC); } const numAllocatedSectionPlanes = scene._sectionPlanesState.getNumAllocatedSectionPlanes(); const numSectionPlanes = scene._sectionPlanesState.sectionPlanes.length; if (numAllocatedSectionPlanes > 0) { const sectionPlanes = scene._sectionPlanesState.sectionPlanes; const baseIndex = dataTextureLayer.layerIndex * numSectionPlanes; const renderFlags = model.renderFlags; for (let sectionPlaneIndex = 0; sectionPlaneIndex < numAllocatedSectionPlanes; sectionPlaneIndex++) { const sectionPlaneUniforms = this._uSectionPlanes[sectionPlaneIndex]; if (sectionPlaneUniforms) { if (sectionPlaneIndex < numSectionPlanes) { const active = renderFlags.sectionPlanesActivePerLayer[baseIndex + sectionPlaneIndex]; gl.uniform1i(sectionPlaneUniforms.active, active ? 1 : 0); if (active) { const sectionPlane = sectionPlanes[sectionPlaneIndex]; if (origin) { const rtcSectionPlanePos = getPlaneRTCPos(sectionPlane.dist, sectionPlane.dir, origin, tempVec3a$o, model.matrix); gl.uniform3fv(sectionPlaneUniforms.pos, rtcSectionPlanePos); } else { gl.uniform3fv(sectionPlaneUniforms.pos, sectionPlane.pos); } gl.uniform3fv(sectionPlaneUniforms.dir, sectionPlane.dir); } } else { gl.uniform1i(sectionPlaneUniforms.active, 0); } } } } if (state.numIndices8Bits > 0) { textureState.bindTriangleIndicesTextures( this._program, this._uTexturePerPolygonIdPortionIds, this._uTexturePerPolygonIdIndices, 8 // 8 bits indices ); gl.drawArrays(gl.TRIANGLES, 0, state.numIndices8Bits); } if (state.numIndices16Bits > 0) { textureState.bindTriangleIndicesTextures( this._program, this._uTexturePerPolygonIdPortionIds, this._uTexturePerPolygonIdIndices, 16 // 16 bits indices ); gl.drawArrays(gl.TRIANGLES, 0, state.numIndices16Bits); } if (state.numIndices32Bits > 0) { textureState.bindTriangleIndicesTextures( this._program, this._uTexturePerPolygonIdPortionIds, this._uTexturePerPolygonIdIndices, 32 // 32 bits indices ); gl.drawArrays(gl.TRIANGLES, 0, state.numIndices32Bits); } frameCtx.drawElements++; } _allocate() { const scene = this._scene; const gl = scene.canvas.gl; this._program = new Program(gl, this._buildShader()); if (this._program.errors) { this.errors = this._program.errors; return; } const program = this._program; this._uRenderPass = program.getLocation("renderPass"); this._uColor = program.getLocation("color"); this._uWorldMatrix = program.getLocation("sceneModelMatrix"); this._uViewMatrix = program.getLocation("viewMatrix"); this._uProjMatrix = program.getLocation("projMatrix"); this._uSectionPlanes = []; for (let i = 0, len = scene._sectionPlanesState.getNumAllocatedSectionPlanes(); i < len; i++) { this._uSectionPlanes.push({ active: program.getLocation("sectionPlaneActive" + i), pos: program.getLocation("sectionPlanePos" + i), dir: program.getLocation("sectionPlaneDir" + i) }); } if (scene.logarithmicDepthBufferEnabled) { this._uLogDepthBufFC = program.getLocation("logDepthBufFC"); } this.uTexturePerObjectPositionsDecodeMatrix = "uObjectPerObjectPositionsDecodeMatrix"; this.uTexturePerObjectColorsAndFlags = "uObjectPerObjectColorsAndFlags"; this._uTexturePerVertexIdCoordinates = "uTexturePerVertexIdCoordinates"; this._uTexturePerPolygonIdNormals = "uTexturePerPolygonIdNormals"; this._uTexturePerPolygonIdIndices = "uTexturePerPolygonIdIndices"; this._uTexturePerPolygonIdPortionIds = "uTexturePerPolygonIdPortionIds"; this._uTexturePerObjectMatrix= "uTexturePerObjectMatrix"; this._uCameraEyeRtc = program.getLocation("uCameraEyeRtc"); } _bindProgram(frameCtx) { const scene = this._scene; const gl = scene.canvas.gl; const project = scene.camera.project; this._program.bind(); if (scene.logarithmicDepthBufferEnabled) { const logDepthBufFC = 2.0 / (Math.log(project.far + 1.0) / Math.LN2); gl.uniform1f(this._uLogDepthBufFC, logDepthBufFC); } } _buildShader() { return { vertex: this._buildVertexShader(), fragment: this._buildFragmentShader() }; } _buildVertexShader() { const scene = this._scene; const clipping = scene._sectionPlanesState.getNumAllocatedSectionPlanes() > 0; const src = []; src.push("#version 300 es"); src.push("// Triangles dataTexture silhouette vertex shader"); src.push("#ifdef GL_FRAGMENT_PRECISION_HIGH"); src.push("precision highp float;"); src.push("precision highp int;"); src.push("precision highp usampler2D;"); src.push("precision highp isampler2D;"); src.push("precision highp sampler2D;"); src.push("#else"); src.push("precision mediump float;"); src.push("precision mediump int;"); src.push("precision mediump usampler2D;"); src.push("precision mediump isampler2D;"); src.push("precision mediump sampler2D;"); src.push("#endif"); src.push("uniform int renderPass;"); if (scene.entityOffsetsEnabled) { src.push("in vec3 offset;"); } src.push("uniform mat4 sceneModelMatrix;"); src.push("uniform mat4 viewMatrix;"); src.push("uniform mat4 projMatrix;"); // src.push("uniform sampler2D uOcclusionTexture;"); src.push("uniform highp sampler2D uObjectPerObjectPositionsDecodeMatrix;"); src.push("uniform highp sampler2D uTexturePerObjectMatrix;"); src.push("uniform lowp usampler2D uObjectPerObjectColorsAndFlags;"); src.push("uniform mediump usampler2D uTexturePerVertexIdCoordinates;"); src.push("uniform highp usampler2D uTexturePerPolygonIdIndices;"); src.push("uniform mediump usampler2D uTexturePerPolygonIdPortionIds;"); src.push("uniform vec3 uCameraEyeRtc;"); src.push("vec3 positions[3];"); if (scene.logarithmicDepthBufferEnabled) { src.push("uniform float logDepthBufFC;"); src.push("out float vFragDepth;"); src.push("out float isPerspective;"); } src.push("bool isPerspectiveMatrix(mat4 m) {"); src.push(" return (m[2][3] == - 1.0);"); src.push("}"); if (clipping) { src.push("out vec4 vWorldPosition;"); src.push("flat out uint vFlags2;"); } src.push("void main(void) {"); // constants src.push("int polygonIndex = gl_VertexID / 3;"); // get packed object-id src.push("int h_packed_object_id_index = (polygonIndex >> 3) & 4095;"); src.push("int v_packed_object_id_index = (polygonIndex >> 3) >> 12;"); src.push("int objectIndex = int(texelFetch(uTexturePerPolygonIdPortionIds, ivec2(h_packed_object_id_index, v_packed_object_id_index), 0).r);"); src.push("ivec2 objectIndexCoords = ivec2(objectIndex % 512, objectIndex / 512);"); // get flags & flags2 src.push("uvec4 flags = texelFetch (uObjectPerObjectColorsAndFlags, ivec2(objectIndexCoords.x*8+2, objectIndexCoords.y), 0);"); src.push("uvec4 flags2 = texelFetch (uObjectPerObjectColorsAndFlags, ivec2(objectIndexCoords.x*8+3, objectIndexCoords.y), 0);"); // flags.y = NOT_RENDERED | SILHOUETTE_HIGHLIGHTED | SILHOUETTE_SELECTED | SILHOUETTE_XRAYED // renderPass = SILHOUETTE_HIGHLIGHTED | SILHOUETTE_SELECTED | | SILHOUETTE_XRAYED src.push(`if (int(flags.y) != renderPass) {`); src.push(" gl_Position = vec4(3.0, 3.0, 3.0, 1.0);"); // Cull vertex src.push(" return;"); // Cull vertex src.push("} else {"); // get vertex base src.push("ivec4 packedVertexBase = ivec4(texelFetch (uObjectPerObjectColorsAndFlags, ivec2(objectIndexCoords.x*8+4, objectIndexCoords.y), 0));"); src.push("ivec4 packedIndexBaseOffset = ivec4(texelFetch (uObjectPerObjectColorsAndFlags, ivec2(objectIndexCoords.x*8+5, objectIndexCoords.y), 0));"); src.push("int indexBaseOffset = (packedIndexBaseOffset.r << 24) + (packedIndexBaseOffset.g << 16) + (packedIndexBaseOffset.b << 8) + packedIndexBaseOffset.a;"); src.push("int h_index = (polygonIndex - indexBaseOffset) & 4095;"); src.push("int v_index = (polygonIndex - indexBaseOffset) >> 12;"); src.push("ivec3 vertexIndices = ivec3(texelFetch(uTexturePerPolygonIdIndices, ivec2(h_index, v_index), 0));"); src.push("ivec3 uniqueVertexIndexes = vertexIndices + (packedVertexBase.r << 24) + (packedVertexBase.g << 16) + (packedVertexBase.b << 8) + packedVertexBase.a;"); src.push("ivec3 indexPositionH = uniqueVertexIndexes & 4095;"); src.push("ivec3 indexPositionV = uniqueVertexIndexes >> 12;"); src.push("mat4 objectInstanceMatrix = mat4 (texelFetch (uTexturePerObjectMatrix, ivec2(objectIndexCoords.x*4+0, objectIndexCoords.y), 0), texelFetch (uTexturePerObjectMatrix, ivec2(objectIndexCoords.x*4+1, objectIndexCoords.y), 0), texelFetch (uTexturePerObjectMatrix, ivec2(objectIndexCoords.x*4+2, objectIndexCoords.y), 0), texelFetch (uTexturePerObjectMatrix, ivec2(objectIndexCoords.x*4+3, objectIndexCoords.y), 0));"); src.push("mat4 objectDecodeAndInstanceMatrix = objectInstanceMatrix * mat4 (texelFetch (uObjectPerObjectPositionsDecodeMatrix, ivec2(objectIndexCoords.x*4+0, objectIndexCoords.y), 0), texelFetch (uObjectPerObjectPositionsDecodeMatrix, ivec2(objectIndexCoords.x*4+1, objectIndexCoords.y), 0), texelFetch (uObjectPerObjectPositionsDecodeMatrix, ivec2(objectIndexCoords.x*4+2, objectIndexCoords.y), 0), texelFetch (uObjectPerObjectPositionsDecodeMatrix, ivec2(objectIndexCoords.x*4+3, objectIndexCoords.y), 0));"); src.push("uint solid = texelFetch (uObjectPerObjectColorsAndFlags, ivec2(objectIndexCoords.x*8+7, objectIndexCoords.y), 0).r;"); // get position src.push("positions[0] = vec3(texelFetch(uTexturePerVertexIdCoordinates, ivec2(indexPositionH.r, indexPositionV.r), 0));"); src.push("positions[1] = vec3(texelFetch(uTexturePerVertexIdCoordinates, ivec2(indexPositionH.g, indexPositionV.g), 0));"); src.push("positions[2] = vec3(texelFetch(uTexturePerVertexIdCoordinates, ivec2(indexPositionH.b, indexPositionV.b), 0));"); // get normal src.push("vec3 normal = normalize(cross(positions[2] - positions[0], positions[1] - positions[0]));"); src.push("vec3 position;"); src.push("position = positions[gl_VertexID % 3];"); src.push("vec3 viewNormal = -normalize((transpose(inverse(viewMatrix*objectDecodeAndInstanceMatrix)) * vec4(normal,1)).xyz);"); // when the geometry is not solid, if needed, flip the triangle winding src.push("if (solid != 1u) {"); src.push("if (isPerspectiveMatrix(projMatrix)) {"); src.push("vec3 uCameraEyeRtcInQuantizedSpace = (inverse(sceneModelMatrix * objectDecodeAndInstanceMatrix) * vec4(uCameraEyeRtc, 1)).xyz;"); src.push("if (dot(position.xyz - uCameraEyeRtcInQuantizedSpace, normal) < 0.0) {"); src.push("position = positions[2 - (gl_VertexID % 3)];"); src.push("viewNormal = -viewNormal;"); src.push("}"); src.push("} else {"); src.push("if (viewNormal.z < 0.0) {"); src.push("position = positions[2 - (gl_VertexID % 3)];"); src.push("viewNormal = -viewNormal;"); src.push("}"); src.push("}"); src.push("}"); src.push("vec4 worldPosition = sceneModelMatrix * (objectDecodeAndInstanceMatrix * vec4(position, 1.0)); "); src.push("vec4 viewPosition = viewMatrix * worldPosition; "); src.push("vec4 clipPos = projMatrix * viewPosition;"); if (scene.logarithmicDepthBufferEnabled) { src.push("vFragDepth = 1.0 + clipPos.w;"); src.push("isPerspective = float (isPerspectiveMatrix(projMatrix));"); } if (clipping) { src.push("vWorldPosition = worldPosition;"); src.push("vFlags2 = flags2.r;"); } src.push("gl_Position = clipPos;"); src.push("}"); src.push("}"); return src; } _buildFragmentShader() { const scene = this._scene; const clipping = scene._sectionPlanesState.getNumAllocatedSectionPlanes() > 0; const src = []; src.push('#version 300 es'); src.push("// Triangles dataTexture draw fragment shader"); src.push("#ifdef GL_FRAGMENT_PRECISION_HIGH"); src.push("precision highp float;"); src.push("precision highp int;"); src.push("#else"); src.push("precision mediump float;"); src.push("precision mediump int;"); src.push("#endif"); if (scene.logarithmicDepthBufferEnabled) { src.push("in float isPerspective;"); src.push("uniform float logDepthBufFC;"); src.push("in float vFragDepth;"); } if (clipping) { src.push("in vec4 vWorldPosition;"); src.push("flat in uint vFlags2;"); for (let i = 0, len = scene._sectionPlanesState.getNumAllocatedSectionPlanes(); i < len; i++) { src.push("uniform bool sectionPlaneActive" + i + ";"); src.push("uniform vec3 sectionPlanePos" + i + ";"); src.push("uniform vec3 sectionPlaneDir" + i + ";"); } src.push("uniform float sliceThickness;"); src.push("uniform vec4 sliceColor;"); } src.push("uniform vec4 color;"); src.push("out vec4 outColor;"); src.push("void main(void) {"); src.push(" vec4 newColor;"); src.push(" newColor = color;"); if (clipping) { src.push(" bool clippable = vFlags2 > 0u;"); src.push(" if (clippable) {"); src.push(" float dist = 0.0;"); for (let i = 0, len = scene._sectionPlanesState.getNumAllocatedSectionPlanes(); i < len; i++) { src.push("if (sectionPlaneActive" + i + ") {"); src.push(" dist += clamp(dot(-sectionPlaneDir" + i + ".xyz, vWorldPosition.xyz - sectionPlanePos" + i + ".xyz), 0.0, 1000.0);"); src.push("}"); } src.push(" if (dist > sliceThickness) { "); src.push(" discard;"); src.push(" }"); src.push(" if (dist > 0.0) { "); src.push(" newColor = sliceColor;"); src.push(" }"); src.push("}"); } if (scene.logarithmicDepthBufferEnabled) { src.push(" gl_FragDepth = isPerspective == 0.0 ? gl_FragCoord.z : log2( vFragDepth ) * logDepthBufFC * 0.5;"); } src.push(" outColor = newColor;"); src.push("}"); return src; } webglContextRestored() { this._program = null; } destroy() { if (this._program) { this._program.destroy(); } this._program = null; } } const defaultColor = new Float32Array([0, 0, 0, 1]); const tempVec3a$n = math.vec3(); const tempVec3b$j = math.vec3(); math.vec3(); const tempMat4a$a = math.mat4(); /** * @private */ class DTXTrianglesEdgesRenderer { constructor(scene) { this._scene = scene; this._hash = this._getHash(); this._allocate(); } getValid() { return this._hash === this._getHash(); }; _getHash() { return this._scene._sectionPlanesState.getHash(); } drawLayer(frameCtx, dataTextureLayer, renderPass) { const model = dataTextureLayer.model; const scene = model.scene; const camera = scene.camera; const gl = scene.canvas.gl; const state = dataTextureLayer._state; const textureState = state.textureState; const origin = dataTextureLayer._state.origin; const {position, rotationMatrix} = model; const viewMatrix = camera.viewMatrix; if (!this._program) { this._allocate(dataTextureLayer); if (this.errors) { return; } } if (frameCtx.lastProgramId !== this._program.id) { frameCtx.lastProgramId = this._program.id; this._bindProgram(); } textureState.bindCommonTextures( this._program, this.uTexturePerObjectPositionsDecodeMatrix, this._uTexturePerVertexIdCoordinates, this.uTexturePerObjectColorsAndFlags, this._uTexturePerObjectMatrix ); let rtcViewMatrix; const gotOrigin = (origin[0] !== 0 || origin[1] !== 0 || origin[2] !== 0); const gotPosition = (position[0] !== 0 || position[1] !== 0 || position[2] !== 0); if (gotOrigin || gotPosition) { const rtcOrigin = tempVec3a$n; if (gotOrigin) { const rotatedOrigin = math.transformPoint3(rotationMatrix, origin, tempVec3b$j); rtcOrigin[0] = rotatedOrigin[0]; rtcOrigin[1] = rotatedOrigin[1]; rtcOrigin[2] = rotatedOrigin[2]; } else { rtcOrigin[0] = 0; rtcOrigin[1] = 0; rtcOrigin[2] = 0; } rtcOrigin[0] += position[0]; rtcOrigin[1] += position[1]; rtcOrigin[2] += position[2]; rtcViewMatrix = createRTCViewMat(viewMatrix, rtcOrigin, tempMat4a$a); } else { rtcViewMatrix = viewMatrix; } gl.uniform1i(this._uRenderPass, renderPass); gl.uniformMatrix4fv(this._uSceneModelMatrix, false, rotationMatrix); gl.uniformMatrix4fv(this._uViewMatrix, false, rtcViewMatrix); gl.uniformMatrix4fv(this._uProjMatrix, false, camera.projMatrix); if (renderPass === RENDER_PASSES.EDGES_XRAYED) { const material = scene.xrayMaterial._state; const edgeColor = material.edgeColor; const edgeAlpha = material.edgeAlpha; gl.uniform4f(this._uColor, edgeColor[0], edgeColor[1], edgeColor[2], edgeAlpha); } else if (renderPass === RENDER_PASSES.EDGES_HIGHLIGHTED) { const material = scene.highlightMaterial._state; const edgeColor = material.edgeColor; const edgeAlpha = material.edgeAlpha; gl.uniform4f(this._uColor, edgeColor[0], edgeColor[1], edgeColor[2], edgeAlpha); } else if (renderPass === RENDER_PASSES.EDGES_SELECTED) { const material = scene.selectedMaterial._state; const edgeColor = material.edgeColor; const edgeAlpha = material.edgeAlpha; gl.uniform4f(this._uColor, edgeColor[0], edgeColor[1], edgeColor[2], edgeAlpha); } else { gl.uniform4fv(this._uColor, defaultColor); } const numAllocatedSectionPlanes = scene._sectionPlanesState.getNumAllocatedSectionPlanes(); const numSectionPlanes = scene._sectionPlanesState.sectionPlanes.length; if (numAllocatedSectionPlanes > 0) { const sectionPlanes = scene._sectionPlanesState.sectionPlanes; const baseIndex = dataTextureLayer.layerIndex * numSectionPlanes; const renderFlags = model.renderFlags; for (let sectionPlaneIndex = 0; sectionPlaneIndex < numAllocatedSectionPlanes; sectionPlaneIndex++) { const sectionPlaneUniforms = this._uSectionPlanes[sectionPlaneIndex]; if (sectionPlaneUniforms) { if (sectionPlaneIndex < numSectionPlanes) { const active = renderFlags.sectionPlanesActivePerLayer[baseIndex + sectionPlaneIndex]; gl.uniform1i(sectionPlaneUniforms.active, active ? 1 : 0); if (active) { const sectionPlane = sectionPlanes[sectionPlaneIndex]; if (origin) { const rtcSectionPlanePos = getPlaneRTCPos(sectionPlane.dist, sectionPlane.dir, origin, tempVec3a$n, model.matrix); gl.uniform3fv(sectionPlaneUniforms.pos, rtcSectionPlanePos); } else { gl.uniform3fv(sectionPlaneUniforms.pos, sectionPlane.pos); } gl.uniform3fv(sectionPlaneUniforms.dir, sectionPlane.dir); } } else { gl.uniform1i(sectionPlaneUniforms.active, 0); } } } } if (state.numEdgeIndices8Bits > 0) { textureState.bindEdgeIndicesTextures( this._program, this._uTexturePerEdgeIdPortionIds, this._uTexturePerPolygonIdEdgeIndices, 8 // 8 bits edge indices ); gl.drawArrays(gl.LINES, 0, state.numEdgeIndices8Bits); } if (state.numEdgeIndices16Bits > 0) { textureState.bindEdgeIndicesTextures( this._program, this._uTexturePerEdgeIdPortionIds, this._uTexturePerPolygonIdEdgeIndices, 16 // 16 bits edge indices ); gl.drawArrays(gl.LINES, 0, state.numEdgeIndices16Bits); } if (state.numEdgeIndices32Bits > 0) { textureState.bindEdgeIndicesTextures( this._program, this._uTexturePerEdgeIdPortionIds, this._uTexturePerPolygonIdEdgeIndices, 32 // 32 bits edge indices ); gl.drawArrays(gl.LINES, 0, state.numEdgeIndices32Bits); } frameCtx.drawElements++; } _allocate() { const scene = this._scene; const gl = scene.canvas.gl; this._program = new Program(gl, this._buildShader()); if (this._program.errors) { this.errors = this._program.errors; return; } const program = this._program; this._uRenderPass = program.getLocation("renderPass"); this._uColor = program.getLocation("color"); this._uSceneModelMatrix = program.getLocation("sceneModelMatrix"); this._uWorldMatrix = program.getLocation("worldMatrix"); this._uViewMatrix = program.getLocation("viewMatrix"); this._uProjMatrix = program.getLocation("projMatrix"); this._uSectionPlanes = []; for (let i = 0, len = scene._sectionPlanesState.getNumAllocatedSectionPlanes(); i < len; i++) { this._uSectionPlanes.push({ active: program.getLocation("sectionPlaneActive" + i), pos: program.getLocation("sectionPlanePos" + i), dir: program.getLocation("sectionPlaneDir" + i) }); } if (scene.logarithmicDepthBufferEnabled) { this._uLogDepthBufFC = program.getLocation("logDepthBufFC"); } this.uTexturePerObjectPositionsDecodeMatrix = "uObjectPerObjectPositionsDecodeMatrix"; this.uTexturePerObjectColorsAndFlags = "uObjectPerObjectColorsAndFlags"; this._uTexturePerVertexIdCoordinates = "uTexturePerVertexIdCoordinates"; this._uTexturePerPolygonIdEdgeIndices = "uTexturePerPolygonIdEdgeIndices"; this._uTexturePerEdgeIdPortionIds = "uTexturePerEdgeIdPortionIds"; this._uTexturePerObjectMatrix= "uTexturePerObjectMatrix"; } _bindProgram() { const scene = this._scene; const gl = scene.canvas.gl; const program = this._program; const project = scene.camera.project; program.bind(); if (scene.logarithmicDepthBufferEnabled) { const logDepthBufFC = 2.0 / (Math.log(project.far + 1.0) / Math.LN2); gl.uniform1f(this._uLogDepthBufFC, logDepthBufFC); } } _buildShader() { return { vertex: this._buildVertexShader(), fragment: this._buildFragmentShader() }; } _buildVertexShader() { const scene = this._scene; const clipping = scene._sectionPlanesState.getNumAllocatedSectionPlanes() > 0; const src = []; src.push("#version 300 es"); src.push("// DTXTrianglesEdgesRenderer vertex shader"); src.push("#ifdef GL_FRAGMENT_PRECISION_HIGH"); src.push("precision highp float;"); src.push("precision highp int;"); src.push("precision highp usampler2D;"); src.push("precision highp isampler2D;"); src.push("precision highp sampler2D;"); src.push("#else"); src.push("precision mediump float;"); src.push("precision mediump int;"); src.push("precision mediump usampler2D;"); src.push("precision mediump isampler2D;"); src.push("precision mediump sampler2D;"); src.push("#endif"); src.push("uniform int renderPass;"); // if (scene.entityOffsetsEnabled) { // src.push("in vec3 offset;"); // } src.push("uniform mat4 sceneModelMatrix;"); src.push("uniform mat4 viewMatrix;"); src.push("uniform mat4 projMatrix;"); src.push("uniform highp sampler2D uObjectPerObjectPositionsDecodeMatrix;"); src.push("uniform highp sampler2D uTexturePerObjectMatrix;"); src.push("uniform lowp usampler2D uObjectPerObjectColorsAndFlags;"); src.push("uniform mediump usampler2D uTexturePerVertexIdCoordinates;"); src.push("uniform highp usampler2D uTexturePerPolygonIdEdgeIndices;"); src.push("uniform mediump usampler2D uTexturePerEdgeIdPortionIds;"); src.push("uniform vec4 color;"); if (scene.logarithmicDepthBufferEnabled) { src.push("uniform float logDepthBufFC;"); src.push("out float vFragDepth;"); src.push("bool isPerspectiveMatrix(mat4 m) {"); src.push(" return (m[2][3] == - 1.0);"); src.push("}"); src.push("out float isPerspective;"); } if (clipping) { src.push("out vec4 vWorldPosition;"); src.push("flat out uint vFlags2;"); } src.push("out vec4 vColor;"); src.push("void main(void) {"); // constants src.push("int edgeIndex = gl_VertexID / 2;"); // get packed object-id src.push("int h_packed_object_id_index = (edgeIndex >> 3) & 4095;"); src.push("int v_packed_object_id_index = (edgeIndex >> 3) >> 12;"); src.push("int objectIndex = int(texelFetch(uTexturePerEdgeIdPortionIds, ivec2(h_packed_object_id_index, v_packed_object_id_index), 0).r);"); src.push("ivec2 objectIndexCoords = ivec2(objectIndex % 512, objectIndex / 512);"); // get flags & flags2 src.push("uvec4 flags = texelFetch (uObjectPerObjectColorsAndFlags, ivec2(objectIndexCoords.x*8+2, objectIndexCoords.y), 0);"); src.push("uvec4 flags2 = texelFetch (uObjectPerObjectColorsAndFlags, ivec2(objectIndexCoords.x*8+3, objectIndexCoords.y), 0);"); // flags.z = NOT_RENDERED | EDGES_COLOR_OPAQUE | EDGES_COLOR_TRANSPARENT | EDGES_HIGHLIGHTED | EDGES_XRAYED | EDGES_SELECTED // renderPass = EDGES_COLOR_OPAQUE | EDGES_COLOR_TRANSPARENT | EDGES_HIGHLIGHTED | EDGES_XRAYED | EDGES_SELECTED src.push(`if (int(flags.z) != renderPass) {`); src.push(" gl_Position = vec4(3.0, 3.0, 3.0, 1.0);"); // Cull vertex src.push(" return;"); // Cull vertex src.push("} else {"); // get vertex base src.push("ivec4 packedVertexBase = ivec4(texelFetch (uObjectPerObjectColorsAndFlags, ivec2(objectIndexCoords.x*8+4, objectIndexCoords.y), 0));"); src.push("ivec4 packedEdgeIndexBaseOffset = ivec4(texelFetch (uObjectPerObjectColorsAndFlags, ivec2(objectIndexCoords.x*8+6, objectIndexCoords.y), 0));"); src.push("int edgeIndexBaseOffset = (packedEdgeIndexBaseOffset.r << 24) + (packedEdgeIndexBaseOffset.g << 16) + (packedEdgeIndexBaseOffset.b << 8) + packedEdgeIndexBaseOffset.a;"); src.push("int h_index = (edgeIndex - edgeIndexBaseOffset) & 4095;"); src.push("int v_index = (edgeIndex - edgeIndexBaseOffset) >> 12;"); src.push("ivec3 vertexIndices = ivec3(texelFetch(uTexturePerPolygonIdEdgeIndices, ivec2(h_index, v_index), 0));"); src.push("ivec3 uniqueVertexIndexes = vertexIndices + (packedVertexBase.r << 24) + (packedVertexBase.g << 16) + (packedVertexBase.b << 8) + packedVertexBase.a;"); src.push("int indexPositionH = uniqueVertexIndexes[gl_VertexID % 2] & 4095;"); src.push("int indexPositionV = uniqueVertexIndexes[gl_VertexID % 2] >> 12;"); src.push("mat4 objectInstanceMatrix = mat4 (texelFetch (uTexturePerObjectMatrix, ivec2(objectIndexCoords.x*4+0, objectIndexCoords.y), 0), texelFetch (uTexturePerObjectMatrix, ivec2(objectIndexCoords.x*4+1, objectIndexCoords.y), 0), texelFetch (uTexturePerObjectMatrix, ivec2(objectIndexCoords.x*4+2, objectIndexCoords.y), 0), texelFetch (uTexturePerObjectMatrix, ivec2(objectIndexCoords.x*4+3, objectIndexCoords.y), 0));"); src.push("mat4 objectDecodeAndInstanceMatrix = objectInstanceMatrix * mat4 (texelFetch (uObjectPerObjectPositionsDecodeMatrix, ivec2(objectIndexCoords.x*4+0, objectIndexCoords.y), 0), texelFetch (uObjectPerObjectPositionsDecodeMatrix, ivec2(objectIndexCoords.x*4+1, objectIndexCoords.y), 0), texelFetch (uObjectPerObjectPositionsDecodeMatrix, ivec2(objectIndexCoords.x*4+2, objectIndexCoords.y), 0), texelFetch (uObjectPerObjectPositionsDecodeMatrix, ivec2(objectIndexCoords.x*4+3, objectIndexCoords.y), 0));"); // get flags & flags2 src.push("uvec4 flags = texelFetch (uObjectPerObjectColorsAndFlags, ivec2(objectIndexCoords.x*8+2, objectIndexCoords.y), 0);"); src.push("uvec4 flags2 = texelFetch (uObjectPerObjectColorsAndFlags, ivec2(objectIndexCoords.x*8+3, objectIndexCoords.y), 0);"); // get position src.push("vec3 position = vec3(texelFetch(uTexturePerVertexIdCoordinates, ivec2(indexPositionH, indexPositionV), 0));"); src.push("mat4 matrix = mat4 (texelFetch (uTexturePerObjectMatrix, ivec2(objectIndexCoords.x*4+0, objectIndexCoords.y), 0), texelFetch (uTexturePerObjectMatrix, ivec2(objectIndexCoords.x*4+1, objectIndexCoords.y), 0), texelFetch (uTexturePerObjectMatrix, ivec2(objectIndexCoords.x*4+2, objectIndexCoords.y), 0), texelFetch (uTexturePerObjectMatrix, ivec2(objectIndexCoords.x*4+3, objectIndexCoords.y), 0));"); src.push("vec4 worldPosition = sceneModelMatrix * (objectDecodeAndInstanceMatrix * vec4(position, 1.0)); "); src.push("vec4 viewPosition = viewMatrix * worldPosition; "); if (clipping) { src.push(" vWorldPosition = worldPosition;"); src.push(" vFlags2 = flags2.r;"); } src.push("vec4 clipPos = projMatrix * viewPosition;"); if (scene.logarithmicDepthBufferEnabled) { src.push("vFragDepth = 1.0 + clipPos.w;"); src.push("isPerspective = float (isPerspectiveMatrix(projMatrix));"); } src.push("gl_Position = clipPos;"); src.push("vColor = vec4(color.r, color.g, color.b, color.a);"); src.push("}"); src.push("}"); return src; } _buildFragmentShader() { const scene = this._scene; const clipping = scene._sectionPlanesState.getNumAllocatedSectionPlanes() > 0; const src = []; src.push('#version 300 es'); src.push("// DTXTrianglesEdgesRenderer fragment shader"); if (scene.logarithmicDepthBufferEnabled) { src.push("#extension GL_EXT_frag_depth : enable"); } src.push("#ifdef GL_FRAGMENT_PRECISION_HIGH"); src.push("precision highp float;"); src.push("precision highp int;"); src.push("#else"); src.push("precision mediump float;"); src.push("precision mediump int;"); src.push("#endif"); if (scene.logarithmicDepthBufferEnabled) { src.push("in float isPerspective;"); src.push("uniform float logDepthBufFC;"); src.push("in float vFragDepth;"); } if (clipping) { src.push("in vec4 vWorldPosition;"); src.push("flat in uint vFlags2;"); for (let i = 0, len = scene._sectionPlanesState.getNumAllocatedSectionPlanes(); i < len; i++) { src.push("uniform bool sectionPlaneActive" + i + ";"); src.push("uniform vec3 sectionPlanePos" + i + ";"); src.push("uniform vec3 sectionPlaneDir" + i + ";"); } src.push("uniform float sliceThickness;"); src.push("uniform vec4 sliceColor;"); } src.push("in vec4 vColor;"); src.push("out vec4 outColor;"); src.push("void main(void) {"); src.push(" vec4 newColor;"); src.push(" newColor = vColor;"); if (clipping) { src.push(" bool clippable = vFlags2 > 0u;"); src.push(" if (clippable) {"); src.push(" float dist = 0.0;"); for (let i = 0, len = scene._sectionPlanesState.getNumAllocatedSectionPlanes(); i < len; i++) { src.push("if (sectionPlaneActive" + i + ") {"); src.push(" dist += clamp(dot(-sectionPlaneDir" + i + ".xyz, vWorldPosition.xyz - sectionPlanePos" + i + ".xyz), 0.0, 1000.0);"); src.push("}"); } src.push(" if (dist > sliceThickness) { "); src.push(" discard;"); src.push(" }"); src.push(" if (dist > 0.0) { "); src.push(" newColor = sliceColor;"); src.push(" }"); src.push("}"); } if (scene.logarithmicDepthBufferEnabled) { src.push(" gl_FragDepth = isPerspective == 0.0 ? gl_FragCoord.z : log2( vFragDepth ) * logDepthBufFC * 0.5;"); } src.push(" outColor = newColor;"); src.push("}"); return src; } webglContextRestored() { this._program = null; } destroy() { if (this._program) { this._program.destroy(); } this._program = null; } } const tempVec3a$m = math.vec3(); const tempVec3b$i = math.vec3(); const tempMat4a$9 = math.mat4(); /** * @private */ class DTXTrianglesEdgesColorRenderer { constructor(scene) { this._scene = scene; this._hash = this._getHash(); this._allocate(); } getValid() { return this._hash === this._getHash(); }; _getHash() { return this._scene._sectionPlanesState.getHash(); } drawLayer(frameCtx, dataTextureLayer, renderPass) { const model = dataTextureLayer.model; const scene = model.scene; const camera = scene.camera; const gl = scene.canvas.gl; const state = dataTextureLayer._state; const textureState = state.textureState; const origin = dataTextureLayer._state.origin; const {position, rotationMatrix} = model; const viewMatrix = camera.viewMatrix; if (!this._program) { this._allocate(); if (this.errors) { return; } } if (frameCtx.lastProgramId !== this._program.id) { frameCtx.lastProgramId = this._program.id; this._bindProgram(); } textureState.bindCommonTextures( this._program, this.uTexturePerObjectPositionsDecodeMatrix, this._uTexturePerVertexIdCoordinates, this.uTexturePerObjectColorsAndFlags, this._uTexturePerObjectMatrix ); let rtcViewMatrix; const gotOrigin = (origin[0] !== 0 || origin[1] !== 0 || origin[2] !== 0); const gotPosition = (position[0] !== 0 || position[1] !== 0 || position[2] !== 0); if (gotOrigin || gotPosition) { const rtcOrigin = tempVec3a$m; if (gotOrigin) { const rotatedOrigin = math.transformPoint3(rotationMatrix, origin, tempVec3b$i); rtcOrigin[0] = rotatedOrigin[0]; rtcOrigin[1] = rotatedOrigin[1]; rtcOrigin[2] = rotatedOrigin[2]; } else { rtcOrigin[0] = 0; rtcOrigin[1] = 0; rtcOrigin[2] = 0; } rtcOrigin[0] += position[0]; rtcOrigin[1] += position[1]; rtcOrigin[2] += position[2]; rtcViewMatrix = createRTCViewMat(viewMatrix, rtcOrigin, tempMat4a$9); } else { rtcViewMatrix = viewMatrix; } gl.uniform1i(this._uRenderPass, renderPass); gl.uniformMatrix4fv(this._uSceneModelMatrix, false, rotationMatrix); gl.uniformMatrix4fv(this._uViewMatrix, false, rtcViewMatrix); gl.uniformMatrix4fv(this._uProjMatrix, false, camera.projMatrix); const numAllocatedSectionPlanes = scene._sectionPlanesState.getNumAllocatedSectionPlanes(); const numSectionPlanes = scene._sectionPlanesState.sectionPlanes.length; if (numAllocatedSectionPlanes > 0) { const sectionPlanes = scene._sectionPlanesState.sectionPlanes; const baseIndex = dataTextureLayer.layerIndex * numSectionPlanes; const renderFlags = model.renderFlags; for (let sectionPlaneIndex = 0; sectionPlaneIndex < numAllocatedSectionPlanes; sectionPlaneIndex++) { const sectionPlaneUniforms = this._uSectionPlanes[sectionPlaneIndex]; if (sectionPlaneUniforms) { if (sectionPlaneIndex < numSectionPlanes) { const active = renderFlags.sectionPlanesActivePerLayer[baseIndex + sectionPlaneIndex]; gl.uniform1i(sectionPlaneUniforms.active, active ? 1 : 0); if (active) { const sectionPlane = sectionPlanes[sectionPlaneIndex]; if (origin) { const rtcSectionPlanePos = getPlaneRTCPos(sectionPlane.dist, sectionPlane.dir, origin, tempVec3a$m, model.matrix); gl.uniform3fv(sectionPlaneUniforms.pos, rtcSectionPlanePos); } else { gl.uniform3fv(sectionPlaneUniforms.pos, sectionPlane.pos); } gl.uniform3fv(sectionPlaneUniforms.dir, sectionPlane.dir); } } else { gl.uniform1i(sectionPlaneUniforms.active, 0); } } } } if (state.numEdgeIndices8Bits > 0) { textureState.bindEdgeIndicesTextures( this._program, this._uTexturePerEdgeIdPortionIds, this._uTexturePerPolygonIdEdgeIndices, 8 // 8 bits edge indices ); gl.drawArrays(gl.LINES, 0, state.numEdgeIndices8Bits); } if (state.numEdgeIndices16Bits > 0) { textureState.bindEdgeIndicesTextures( this._program, this._uTexturePerEdgeIdPortionIds, this._uTexturePerPolygonIdEdgeIndices, 16 // 16 bits edge indices ); gl.drawArrays(gl.LINES, 0, state.numEdgeIndices16Bits); } if (state.numEdgeIndices32Bits > 0) { textureState.bindEdgeIndicesTextures( this._program, this._uTexturePerEdgeIdPortionIds, this._uTexturePerPolygonIdEdgeIndices, 32 // 32 bits edge indices ); gl.drawArrays(gl.LINES, 0, state.numEdgeIndices32Bits); } frameCtx.drawElements++; } _allocate() { const scene = this._scene; const gl = scene.canvas.gl; this._program = new Program(gl, this._buildShader()); if (this._program.errors) { this.errors = this._program.errors; return; } const program = this._program; this._uRenderPass = program.getLocation("renderPass"); this._uSceneModelMatrix = program.getLocation("sceneModelMatrix"); this._uViewMatrix = program.getLocation("viewMatrix"); this._uProjMatrix = program.getLocation("projMatrix"); this._uSectionPlanes = []; for (let i = 0, len = scene._sectionPlanesState.getNumAllocatedSectionPlanes(); i < len; i++) { this._uSectionPlanes.push({ active: program.getLocation("sectionPlaneActive" + i), pos: program.getLocation("sectionPlanePos" + i), dir: program.getLocation("sectionPlaneDir" + i) }); } //this._aOffset = program.getAttribute("offset"); if (scene.logarithmicDepthBufferEnabled) { this._uLogDepthBufFC = program.getLocation("logDepthBufFC"); } this.uTexturePerObjectPositionsDecodeMatrix = "uObjectPerObjectPositionsDecodeMatrix"; this.uTexturePerObjectColorsAndFlags = "uObjectPerObjectColorsAndFlags"; this._uTexturePerVertexIdCoordinates = "uTexturePerVertexIdCoordinates"; this._uTexturePerPolygonIdEdgeIndices = "uTexturePerPolygonIdEdgeIndices"; this._uTexturePerEdgeIdPortionIds = "uTexturePerEdgeIdPortionIds"; this._uTexturePerObjectMatrix = "uTexturePerObjectMatrix"; } _bindProgram() { const scene = this._scene; const gl = scene.canvas.gl; const program = this._program; const project = scene.camera.project; program.bind(); if (scene.logarithmicDepthBufferEnabled) { const logDepthBufFC = 2.0 / (Math.log(project.far + 1.0) / Math.LN2); gl.uniform1f(this._uLogDepthBufFC, logDepthBufFC); } } _buildShader() { return { vertex: this._buildVertexShader(), fragment: this._buildFragmentShader() }; } _buildVertexShader() { const scene = this._scene; const clipping = scene._sectionPlanesState.getNumAllocatedSectionPlanes() > 0; const src = []; src.push("#version 300 es"); src.push("// TrianglesDataTextureEdgesColorRenderer"); src.push("#ifdef GL_FRAGMENT_PRECISION_HIGH"); src.push("precision highp float;"); src.push("precision highp int;"); src.push("precision highp usampler2D;"); src.push("precision highp isampler2D;"); src.push("precision highp sampler2D;"); src.push("#else"); src.push("precision mediump float;"); src.push("precision mediump int;"); src.push("precision mediump usampler2D;"); src.push("precision mediump isampler2D;"); src.push("precision mediump sampler2D;"); src.push("#endif"); src.push("uniform int renderPass;"); if (scene.entityOffsetsEnabled) ; src.push("uniform mat4 sceneModelMatrix;"); src.push("uniform mat4 viewMatrix;"); src.push("uniform mat4 projMatrix;"); src.push("uniform highp sampler2D uObjectPerObjectPositionsDecodeMatrix;"); src.push("uniform highp sampler2D uTexturePerObjectMatrix;"); src.push("uniform lowp usampler2D uObjectPerObjectColorsAndFlags;"); src.push("uniform highp sampler2D uObjectPerObjectOffsets;"); src.push("uniform mediump usampler2D uTexturePerVertexIdCoordinates;"); src.push("uniform highp usampler2D uTexturePerPolygonIdEdgeIndices;"); src.push("uniform mediump usampler2D uTexturePerEdgeIdPortionIds;"); // src.push("uniform vec4 color;"); if (scene.logarithmicDepthBufferEnabled) { src.push("uniform float logDepthBufFC;"); src.push("out float vFragDepth;"); src.push("bool isPerspectiveMatrix(mat4 m) {"); src.push(" return (m[2][3] == - 1.0);"); src.push("}"); src.push("out float isPerspective;"); } if (clipping) { src.push("out vec4 vWorldPosition;"); src.push("flat out uint vFlags2;"); } src.push("out vec4 vColor;"); src.push("void main(void) {"); // constants src.push("int edgeIndex = gl_VertexID / 2;"); // get packed object-id src.push("int h_packed_object_id_index = (edgeIndex >> 3) & 4095;"); src.push("int v_packed_object_id_index = (edgeIndex >> 3) >> 12;"); src.push("int objectIndex = int(texelFetch(uTexturePerEdgeIdPortionIds, ivec2(h_packed_object_id_index, v_packed_object_id_index), 0).r);"); src.push("ivec2 objectIndexCoords = ivec2(objectIndex % 512, objectIndex / 512);"); // get flags & flags2 src.push("uvec4 flags = texelFetch (uObjectPerObjectColorsAndFlags, ivec2(objectIndexCoords.x*8+2, objectIndexCoords.y), 0);"); src.push("uvec4 flags2 = texelFetch (uObjectPerObjectColorsAndFlags, ivec2(objectIndexCoords.x*8+3, objectIndexCoords.y), 0);"); // flags.z = NOT_RENDERED | EDGES_COLOR_OPAQUE | EDGES_COLOR_TRANSPARENT | EDGES_HIGHLIGHTED | EDGES_XRAYED | EDGES_SELECTED // renderPass = EDGES_COLOR_OPAQUE | EDGES_COLOR_TRANSPARENT src.push(`if (int(flags.z) != renderPass) {`); src.push(" gl_Position = vec4(3.0, 3.0, 3.0, 1.0);"); // Cull vertex src.push(" return;"); // Cull vertex src.push("} else {"); // get vertex base src.push("ivec4 packedVertexBase = ivec4(texelFetch (uObjectPerObjectColorsAndFlags, ivec2(objectIndexCoords.x*8+4, objectIndexCoords.y), 0));"); src.push("ivec4 packedEdgeIndexBaseOffset = ivec4(texelFetch (uObjectPerObjectColorsAndFlags, ivec2(objectIndexCoords.x*8+6, objectIndexCoords.y), 0));"); src.push("int edgeIndexBaseOffset = (packedEdgeIndexBaseOffset.r << 24) + (packedEdgeIndexBaseOffset.g << 16) + (packedEdgeIndexBaseOffset.b << 8) + packedEdgeIndexBaseOffset.a;"); src.push("int h_index = (edgeIndex - edgeIndexBaseOffset) & 4095;"); src.push("int v_index = (edgeIndex - edgeIndexBaseOffset) >> 12;"); src.push("ivec3 vertexIndices = ivec3(texelFetch(uTexturePerPolygonIdEdgeIndices, ivec2(h_index, v_index), 0));"); src.push("ivec3 uniqueVertexIndexes = vertexIndices + (packedVertexBase.r << 24) + (packedVertexBase.g << 16) + (packedVertexBase.b << 8) + packedVertexBase.a;"); src.push("int indexPositionH = uniqueVertexIndexes[gl_VertexID % 2] & 4095;"); src.push("int indexPositionV = uniqueVertexIndexes[gl_VertexID % 2] >> 12;"); src.push("mat4 objectInstanceMatrix = mat4 (texelFetch (uTexturePerObjectMatrix, ivec2(objectIndexCoords.x*4+0, objectIndexCoords.y), 0), texelFetch (uTexturePerObjectMatrix, ivec2(objectIndexCoords.x*4+1, objectIndexCoords.y), 0), texelFetch (uTexturePerObjectMatrix, ivec2(objectIndexCoords.x*4+2, objectIndexCoords.y), 0), texelFetch (uTexturePerObjectMatrix, ivec2(objectIndexCoords.x*4+3, objectIndexCoords.y), 0));"); src.push("mat4 objectDecodeAndInstanceMatrix = objectInstanceMatrix * mat4 (texelFetch (uObjectPerObjectPositionsDecodeMatrix, ivec2(objectIndexCoords.x*4+0, objectIndexCoords.y), 0), texelFetch (uObjectPerObjectPositionsDecodeMatrix, ivec2(objectIndexCoords.x*4+1, objectIndexCoords.y), 0), texelFetch (uObjectPerObjectPositionsDecodeMatrix, ivec2(objectIndexCoords.x*4+2, objectIndexCoords.y), 0), texelFetch (uObjectPerObjectPositionsDecodeMatrix, ivec2(objectIndexCoords.x*4+3, objectIndexCoords.y), 0));"); // get flags & flags2 src.push("uvec4 flags = texelFetch (uObjectPerObjectColorsAndFlags, ivec2(objectIndexCoords.x*8+2, objectIndexCoords.y), 0);"); src.push("uvec4 flags2 = texelFetch (uObjectPerObjectColorsAndFlags, ivec2(objectIndexCoords.x*8+3, objectIndexCoords.y), 0);"); // get position src.push("vec3 position = vec3(texelFetch(uTexturePerVertexIdCoordinates, ivec2(indexPositionH, indexPositionV), 0));"); // get color src.push("uvec4 color = texelFetch (uObjectPerObjectColorsAndFlags, ivec2(objectIndexCoords.x*8+0, objectIndexCoords.y), 0);"); src.push(`if (color.a == 0u) {`); src.push(" gl_Position = vec4(3.0, 3.0, 3.0, 1.0);"); // Cull vertex src.push(" return;"); src.push("};"); src.push("vec4 worldPosition = sceneModelMatrix * (objectDecodeAndInstanceMatrix * vec4(position, 1.0)); "); src.push("vec4 viewPosition = viewMatrix * worldPosition; "); if (clipping) { src.push(" vWorldPosition = worldPosition;"); src.push(" vFlags2 = flags2.r;"); } src.push("vec4 clipPos = projMatrix * viewPosition;"); if (scene.logarithmicDepthBufferEnabled) { src.push("vFragDepth = 1.0 + clipPos.w;"); src.push("isPerspective = float (isPerspectiveMatrix(projMatrix));"); } src.push("gl_Position = clipPos;"); src.push("vec4 rgb = vec4(color.rgba);"); //src.push("vColor = vec4(float(color.r-100.0) / 255.0, float(color.g-100.0) / 255.0, float(color.b-100.0) / 255.0, float(color.a) / 255.0);"); src.push("vColor = vec4(float(rgb.r*0.5) / 255.0, float(rgb.g*0.5) / 255.0, float(rgb.b*0.5) / 255.0, float(rgb.a) / 255.0);"); src.push("}"); src.push("}"); return src; } _buildFragmentShader() { const scene = this._scene; const clipping = scene._sectionPlanesState.getNumAllocatedSectionPlanes() > 0; const src = []; src.push('#version 300 es'); src.push("// TrianglesDataTextureEdgesColorRenderer"); src.push("#ifdef GL_FRAGMENT_PRECISION_HIGH"); src.push("precision highp float;"); src.push("precision highp int;"); src.push("#else"); src.push("precision mediump float;"); src.push("precision mediump int;"); src.push("#endif"); if (scene.logarithmicDepthBufferEnabled) { src.push("in float isPerspective;"); src.push("uniform float logDepthBufFC;"); src.push("in float vFragDepth;"); } if (clipping) { src.push("in vec4 vWorldPosition;"); src.push("flat in uint vFlags2;"); for (let i = 0, len = scene._sectionPlanesState.getNumAllocatedSectionPlanes(); i < len; i++) { src.push("uniform bool sectionPlaneActive" + i + ";"); src.push("uniform vec3 sectionPlanePos" + i + ";"); src.push("uniform vec3 sectionPlaneDir" + i + ";"); } src.push("uniform float sliceThickness;"); src.push("uniform vec4 sliceColor;"); } src.push("in vec4 vColor;"); src.push("out vec4 outColor;"); src.push("void main(void) {"); src.push(" vec4 newColor;"); src.push(" newColor = vColor;"); if (clipping) { src.push(" bool clippable = vFlags2 > 0u;"); src.push(" if (clippable) {"); src.push(" float dist = 0.0;"); for (let i = 0, len = scene._sectionPlanesState.getNumAllocatedSectionPlanes(); i < len; i++) { src.push("if (sectionPlaneActive" + i + ") {"); src.push(" dist += clamp(dot(-sectionPlaneDir" + i + ".xyz, vWorldPosition.xyz - sectionPlanePos" + i + ".xyz), 0.0, 1000.0);"); src.push("}"); } src.push(" if (dist > sliceThickness) { "); src.push(" discard;"); src.push(" }"); src.push(" if (dist > 0.0) { "); src.push(" newColor = sliceColor;"); src.push(" }"); src.push("}"); } if (scene.logarithmicDepthBufferEnabled) { src.push(" gl_FragDepth = isPerspective == 0.0 ? gl_FragCoord.z : log2( vFragDepth ) * logDepthBufFC * 0.5;"); } src.push(" outColor = newColor;"); src.push("}"); return src; } webglContextRestored() { this._program = null; } destroy() { if (this._program) { this._program.destroy(); } this._program = null; } } const tempVec3a$l = math.vec3(); const tempVec3b$h = math.vec3(); const tempVec3c$f = math.vec3(); const tempMat4a$8 = math.mat4(); /** * @private */ class DTXTrianglesPickMeshRenderer { constructor(scene) { this._scene = scene; this._hash = this._getHash(); this._allocate(); } getValid() { return this._hash === this._getHash(); } _getHash() { return this._scene._sectionPlanesState.getHash(); } drawLayer(frameCtx, dataTextureLayer, renderPass) { if (!this._program) { this._allocate(dataTextureLayer); if (this.errors) { return; } } if (frameCtx.lastProgramId !== this._program.id) { frameCtx.lastProgramId = this._program.id; this._bindProgram(frameCtx); } const model = dataTextureLayer.model; const scene = model.scene; const camera = scene.camera; const gl = scene.canvas.gl; const state = dataTextureLayer._state; const textureState = state.textureState; const origin = dataTextureLayer._state.origin; const {position, rotationMatrix} = model; const viewMatrix = frameCtx.pickViewMatrix || camera.viewMatrix; const projMatrix = frameCtx.pickProjMatrix || camera.projMatrix; const eye = frameCtx.pickOrigin || camera.eye; const far = frameCtx.pickProjMatrix ? frameCtx.pickZFar : camera.project.far; textureState.bindCommonTextures( this._program, this.uTexturePerObjectPositionsDecodeMatrix, this._uTexturePerVertexIdCoordinates, this.uTexturePerObjectColorsAndFlags, this._uTexturePerObjectMatrix ); let rtcViewMatrix; let rtcCameraEye; const gotOrigin = (origin[0] !== 0 || origin[1] !== 0 || origin[2] !== 0); const gotPosition = (position[0] !== 0 || position[1] !== 0 || position[2] !== 0); if (gotOrigin || gotPosition) { const rtcOrigin = tempVec3a$l; if (gotOrigin) { const rotatedOrigin = math.transformPoint3(rotationMatrix, origin, tempVec3b$h); rtcOrigin[0] = rotatedOrigin[0]; rtcOrigin[1] = rotatedOrigin[1]; rtcOrigin[2] = rotatedOrigin[2]; } else { rtcOrigin[0] = 0; rtcOrigin[1] = 0; rtcOrigin[2] = 0; } rtcOrigin[0] += position[0]; rtcOrigin[1] += position[1]; rtcOrigin[2] += position[2]; rtcViewMatrix = createRTCViewMat(viewMatrix, rtcOrigin, tempMat4a$8); rtcCameraEye = tempVec3c$f; rtcCameraEye[0] = eye[0] - rtcOrigin[0]; rtcCameraEye[1] = eye[1] - rtcOrigin[1]; rtcCameraEye[2] = eye[2] - rtcOrigin[2]; } else { rtcViewMatrix = viewMatrix; rtcCameraEye = eye; } gl.uniform2fv(this._uPickClipPos, frameCtx.pickClipPos); gl.uniform2f(this._uDrawingBufferSize, gl.drawingBufferWidth, gl.drawingBufferHeight); gl.uniformMatrix4fv(this._uSceneModelMatrix, false, rotationMatrix); gl.uniformMatrix4fv(this._uViewMatrix, false, rtcViewMatrix); gl.uniformMatrix4fv(this._uProjMatrix, false, projMatrix); gl.uniform3fv(this._uCameraEyeRtc, rtcCameraEye); gl.uniform1i(this._uRenderPass, renderPass); if (scene.logarithmicDepthBufferEnabled) { const logDepthBufFC = 2.0 / (Math.log(far + 1.0) / Math.LN2); gl.uniform1f(this._uLogDepthBufFC, logDepthBufFC); } const numAllocatedSectionPlanes = scene._sectionPlanesState.getNumAllocatedSectionPlanes(); const numSectionPlanes = scene._sectionPlanesState.sectionPlanes.length; if (numAllocatedSectionPlanes > 0) { const sectionPlanes = scene._sectionPlanesState.sectionPlanes; const baseIndex = dataTextureLayer.layerIndex * numSectionPlanes; const renderFlags = model.renderFlags; for (let sectionPlaneIndex = 0; sectionPlaneIndex < numAllocatedSectionPlanes; sectionPlaneIndex++) { const sectionPlaneUniforms = this._uSectionPlanes[sectionPlaneIndex]; if (sectionPlaneUniforms) { if (sectionPlaneIndex < numSectionPlanes) { const active = renderFlags.sectionPlanesActivePerLayer[baseIndex + sectionPlaneIndex]; gl.uniform1i(sectionPlaneUniforms.active, active ? 1 : 0); if (active) { const sectionPlane = sectionPlanes[sectionPlaneIndex]; if (origin) { const rtcSectionPlanePos = getPlaneRTCPos(sectionPlane.dist, sectionPlane.dir, origin, tempVec3a$l, model.matrix); gl.uniform3fv(sectionPlaneUniforms.pos, rtcSectionPlanePos); } else { gl.uniform3fv(sectionPlaneUniforms.pos, sectionPlane.pos); } gl.uniform3fv(sectionPlaneUniforms.dir, sectionPlane.dir); } } else { gl.uniform1i(sectionPlaneUniforms.active, 0); } } } } if (state.numIndices8Bits > 0) { textureState.bindTriangleIndicesTextures( this._program, this._uTexturePerPolygonIdPortionIds, this._uTexturePerPolygonIdIndices, 8 // 8 bits indices ); gl.drawArrays(gl.TRIANGLES, 0, state.numIndices8Bits); } if (state.numIndices16Bits > 0) { textureState.bindTriangleIndicesTextures( this._program, this._uTexturePerPolygonIdPortionIds, this._uTexturePerPolygonIdIndices, 16 // 16 bits indices ); gl.drawArrays(gl.TRIANGLES, 0, state.numIndices16Bits); } if (state.numIndices32Bits > 0) { textureState.bindTriangleIndicesTextures( this._program, this._uTexturePerPolygonIdPortionIds, this._uTexturePerPolygonIdIndices, 32 // 32 bits indices ); gl.drawArrays(gl.TRIANGLES, 0, state.numIndices32Bits); } frameCtx.drawElements++; } _allocate() { const scene = this._scene; const gl = scene.canvas.gl; this._program = new Program(gl, this._buildShader()); if (this._program.errors) { this.errors = this._program.errors; return; } const program = this._program; this._uRenderPass = program.getLocation("renderPass"); this._uPickInvisible = program.getLocation("pickInvisible"); this._uPickClipPos = program.getLocation("pickClipPos"); this._uDrawingBufferSize = program.getLocation("drawingBufferSize"); this._uSceneModelMatrix = program.getLocation("sceneModelMatrix"); this._uViewMatrix = program.getLocation("viewMatrix"); this._uProjMatrix = program.getLocation("projMatrix"); this._uSectionPlanes = []; for (let i = 0, len = scene._sectionPlanesState.getNumAllocatedSectionPlanes(); i < len; i++) { this._uSectionPlanes.push({ active: program.getLocation("sectionPlaneActive" + i), pos: program.getLocation("sectionPlanePos" + i), dir: program.getLocation("sectionPlaneDir" + i) }); } if (scene.logarithmicDepthBufferEnabled) { this._uLogDepthBufFC = program.getLocation("logDepthBufFC"); } this.uTexturePerObjectPositionsDecodeMatrix = "uObjectPerObjectPositionsDecodeMatrix"; this.uTexturePerObjectColorsAndFlags = "uObjectPerObjectColorsAndFlags"; this._uTexturePerVertexIdCoordinates = "uTexturePerVertexIdCoordinates"; this._uTexturePerPolygonIdNormals = "uTexturePerPolygonIdNormals"; this._uTexturePerPolygonIdIndices = "uTexturePerPolygonIdIndices"; this._uTexturePerPolygonIdPortionIds = "uTexturePerPolygonIdPortionIds"; this._uTexturePerObjectMatrix = "uTexturePerObjectMatrix"; this._uCameraEyeRtc = program.getLocation("uCameraEyeRtc"); } _bindProgram(frameCtx) { const scene = this._scene; const gl = scene.canvas.gl; this._program.bind(); gl.uniform1i(this._uPickInvisible, frameCtx.pickInvisible); } _buildShader() { return { vertex: this._buildVertexShader(), fragment: this._buildFragmentShader() }; } _buildVertexShader() { const scene = this._scene; const clipping = scene._sectionPlanesState.getNumAllocatedSectionPlanes() > 0; const src = []; src.push("#version 300 es"); src.push("// Batched geometry picking vertex shader"); src.push("#ifdef GL_FRAGMENT_PRECISION_HIGH"); src.push("precision highp float;"); src.push("precision highp int;"); src.push("precision highp usampler2D;"); src.push("precision highp isampler2D;"); src.push("precision highp sampler2D;"); src.push("#else"); src.push("precision mediump float;"); src.push("precision mediump int;"); src.push("precision mediump usampler2D;"); src.push("precision mediump isampler2D;"); src.push("precision mediump sampler2D;"); src.push("#endif"); src.push("uniform int renderPass;"); if (scene.entityOffsetsEnabled) { src.push("in vec3 offset;"); } src.push("uniform mat4 sceneModelMatrix;"); src.push("uniform mat4 viewMatrix;"); src.push("uniform mat4 projMatrix;"); src.push("uniform bool pickInvisible;"); // src.push("uniform sampler2D uOcclusionTexture;"); src.push("uniform highp sampler2D uObjectPerObjectPositionsDecodeMatrix;"); src.push("uniform highp sampler2D uTexturePerObjectMatrix;"); src.push("uniform lowp usampler2D uObjectPerObjectColorsAndFlags;"); src.push("uniform mediump usampler2D uTexturePerVertexIdCoordinates;"); src.push("uniform highp usampler2D uTexturePerPolygonIdIndices;"); src.push("uniform mediump usampler2D uTexturePerPolygonIdPortionIds;"); src.push("uniform vec3 uCameraEyeRtc;"); src.push("vec3 positions[3];"); if (scene.logarithmicDepthBufferEnabled) { src.push("uniform float logDepthBufFC;"); src.push("out float vFragDepth;"); src.push("out float isPerspective;"); } src.push("uniform vec2 pickClipPos;"); src.push("uniform vec2 drawingBufferSize;"); src.push("vec4 remapClipPos(vec4 clipPos) {"); src.push(" clipPos.xy /= clipPos.w;"); src.push(` clipPos.xy = (clipPos.xy - pickClipPos) * drawingBufferSize;`); src.push(" clipPos.xy *= clipPos.w;"); src.push(" return clipPos;"); src.push("}"); src.push("bool isPerspectiveMatrix(mat4 m) {"); src.push(" return (m[2][3] == - 1.0);"); src.push("}"); if (clipping) { src.push("smooth out vec4 vWorldPosition;"); src.push("flat out uvec4 vFlags2;"); } src.push("out vec4 vPickColor;"); src.push("void main(void) {"); src.push("int polygonIndex = gl_VertexID / 3;"); // get packed object-id src.push("int h_packed_object_id_index = (polygonIndex >> 3) & 4095;"); src.push("int v_packed_object_id_index = (polygonIndex >> 3) >> 12;"); src.push("int objectIndex = int(texelFetch(uTexturePerPolygonIdPortionIds, ivec2(h_packed_object_id_index, v_packed_object_id_index), 0).r);"); src.push("ivec2 objectIndexCoords = ivec2(objectIndex % 512, objectIndex / 512);"); // get flags & flags2 src.push("uvec4 flags = texelFetch (uObjectPerObjectColorsAndFlags, ivec2(objectIndexCoords.x*8+2, objectIndexCoords.y), 0);"); src.push("uvec4 flags2 = texelFetch (uObjectPerObjectColorsAndFlags, ivec2(objectIndexCoords.x*8+3, objectIndexCoords.y), 0);"); // flags.w = NOT_RENDERED | PICK // renderPass = PICK src.push(`if (int(flags.w) != renderPass) {`); src.push(" gl_Position = vec4(3.0, 3.0, 3.0, 1.0);"); // Cull vertex src.push(" return;"); // Cull vertex src.push("} else {"); // get vertex base src.push("ivec4 packedVertexBase = ivec4(texelFetch (uObjectPerObjectColorsAndFlags, ivec2(objectIndexCoords.x*8+4, objectIndexCoords.y), 0));"); src.push("ivec4 packedIndexBaseOffset = ivec4(texelFetch (uObjectPerObjectColorsAndFlags, ivec2(objectIndexCoords.x*8+5, objectIndexCoords.y), 0));"); src.push("int indexBaseOffset = (packedIndexBaseOffset.r << 24) + (packedIndexBaseOffset.g << 16) + (packedIndexBaseOffset.b << 8) + packedIndexBaseOffset.a;"); src.push("int h_index = (polygonIndex - indexBaseOffset) & 4095;"); src.push("int v_index = (polygonIndex - indexBaseOffset) >> 12;"); src.push("ivec3 vertexIndices = ivec3(texelFetch(uTexturePerPolygonIdIndices, ivec2(h_index, v_index), 0));"); src.push("ivec3 uniqueVertexIndexes = vertexIndices + (packedVertexBase.r << 24) + (packedVertexBase.g << 16) + (packedVertexBase.b << 8) + packedVertexBase.a;"); src.push("ivec3 indexPositionH = uniqueVertexIndexes & 4095;"); src.push("ivec3 indexPositionV = uniqueVertexIndexes >> 12;"); src.push("mat4 objectInstanceMatrix = mat4 (texelFetch (uTexturePerObjectMatrix, ivec2(objectIndexCoords.x*4+0, objectIndexCoords.y), 0), texelFetch (uTexturePerObjectMatrix, ivec2(objectIndexCoords.x*4+1, objectIndexCoords.y), 0), texelFetch (uTexturePerObjectMatrix, ivec2(objectIndexCoords.x*4+2, objectIndexCoords.y), 0), texelFetch (uTexturePerObjectMatrix, ivec2(objectIndexCoords.x*4+3, objectIndexCoords.y), 0));"); src.push("mat4 objectDecodeAndInstanceMatrix = objectInstanceMatrix * mat4 (texelFetch (uObjectPerObjectPositionsDecodeMatrix, ivec2(objectIndexCoords.x*4+0, objectIndexCoords.y), 0), texelFetch (uObjectPerObjectPositionsDecodeMatrix, ivec2(objectIndexCoords.x*4+1, objectIndexCoords.y), 0), texelFetch (uObjectPerObjectPositionsDecodeMatrix, ivec2(objectIndexCoords.x*4+2, objectIndexCoords.y), 0), texelFetch (uObjectPerObjectPositionsDecodeMatrix, ivec2(objectIndexCoords.x*4+3, objectIndexCoords.y), 0));"); src.push("uint solid = texelFetch (uObjectPerObjectColorsAndFlags, ivec2(objectIndexCoords.x*8+7, objectIndexCoords.y), 0).r;"); // get position src.push("positions[0] = vec3(texelFetch(uTexturePerVertexIdCoordinates, ivec2(indexPositionH.r, indexPositionV.r), 0));"); src.push("positions[1] = vec3(texelFetch(uTexturePerVertexIdCoordinates, ivec2(indexPositionH.g, indexPositionV.g), 0));"); src.push("positions[2] = vec3(texelFetch(uTexturePerVertexIdCoordinates, ivec2(indexPositionH.b, indexPositionV.b), 0));"); // get color src.push("uvec4 color = texelFetch (uObjectPerObjectColorsAndFlags, ivec2(objectIndexCoords.x*8+0, objectIndexCoords.y), 0);"); // get pick-color src.push("vPickColor = vec4(texelFetch (uObjectPerObjectColorsAndFlags, ivec2(objectIndexCoords.x*8+1, objectIndexCoords.y), 0)) / 255.0;"); // get normal src.push("vec3 normal = normalize(cross(positions[2] - positions[0], positions[1] - positions[0]));"); src.push("vec3 position;"); src.push("position = positions[gl_VertexID % 3];"); // when the geometry is not solid, if needed, flip the triangle winding src.push("if (solid != 1u) {"); src.push("if (isPerspectiveMatrix(projMatrix)) {"); src.push("vec3 uCameraEyeRtcInQuantizedSpace = (inverse(sceneModelMatrix * objectDecodeAndInstanceMatrix) * vec4(uCameraEyeRtc, 1)).xyz;"); src.push("if (dot(position.xyz - uCameraEyeRtcInQuantizedSpace, normal) < 0.0) {"); src.push("position = positions[2 - (gl_VertexID % 3)];"); src.push("}"); src.push("} else {"); src.push("vec3 viewNormal = -normalize((transpose(inverse(viewMatrix*objectDecodeAndInstanceMatrix)) * vec4(normal,1)).xyz);"); src.push("if (viewNormal.z < 0.0) {"); src.push("position = positions[2 - (gl_VertexID % 3)];"); src.push("}"); src.push("}"); src.push("}"); src.push("vec4 worldPosition = sceneModelMatrix * (objectDecodeAndInstanceMatrix * vec4(position, 1.0)); "); src.push("vec4 viewPosition = viewMatrix * worldPosition; "); if (clipping) { src.push(" vWorldPosition = worldPosition;"); src.push(" vFlags2 = flags2;"); } src.push("vec4 clipPos = projMatrix * viewPosition;"); if (scene.logarithmicDepthBufferEnabled) { src.push("vFragDepth = 1.0 + clipPos.w;"); src.push("isPerspective = float (isPerspectiveMatrix(projMatrix));"); } src.push("gl_Position = remapClipPos(clipPos);"); src.push(" }"); src.push("}"); return src; } _buildFragmentShader() { const scene = this._scene; const clipping = scene._sectionPlanesState.getNumAllocatedSectionPlanes() > 0; const src = []; src.push('#version 300 es'); src.push("// Batched geometry picking fragment shader"); src.push("#ifdef GL_FRAGMENT_PRECISION_HIGH"); src.push("precision highp float;"); src.push("precision highp int;"); src.push("#else"); src.push("precision mediump float;"); src.push("precision mediump int;"); src.push("#endif"); if (scene.logarithmicDepthBufferEnabled) { src.push("in float isPerspective;"); src.push("uniform float logDepthBufFC;"); src.push("in float vFragDepth;"); } if (clipping) { src.push("in vec4 vWorldPosition;"); src.push("flat in uvec4 vFlags2;"); for (var i = 0; i < scene._sectionPlanesState.getNumAllocatedSectionPlanes(); i++) { src.push("uniform bool sectionPlaneActive" + i + ";"); src.push("uniform vec3 sectionPlanePos" + i + ";"); src.push("uniform vec3 sectionPlaneDir" + i + ";"); } } src.push("in vec4 vPickColor;"); src.push("out vec4 outPickColor;"); src.push("void main(void) {"); if (clipping) { src.push(" bool clippable = (float(vFlags2.x) > 0.0);"); src.push(" if (clippable) {"); src.push(" float dist = 0.0;"); for (var i = 0; i < scene._sectionPlanesState.getNumAllocatedSectionPlanes(); i++) { src.push(" if (sectionPlaneActive" + i + ") {"); src.push(" dist += clamp(dot(-sectionPlaneDir" + i + ".xyz, vWorldPosition.xyz - sectionPlanePos" + i + ".xyz), 0.0, 1000.0);"); src.push(" }"); } src.push(" if (dist > 0.0) { discard; }"); src.push(" }"); } if (scene.logarithmicDepthBufferEnabled) { src.push(" gl_FragDepth = isPerspective == 0.0 ? gl_FragCoord.z : log2( vFragDepth ) * logDepthBufFC * 0.5;"); //src.push(" gl_FragDepth = log2( vFragDepth ) * logDepthBufFC * 0.5;"); } src.push(" outPickColor = vPickColor; "); src.push("}"); return src; } webglContextRestored() { this._program = null; } destroy() { if (this._program) { this._program.destroy(); } this._program = null; } } const tempVec3a$k = math.vec3(); const tempVec3b$g = math.vec3(); const tempVec3c$e = math.vec3(); math.vec3(); const tempMat4a$7 = math.mat4(); /** * @private */ class DTXTrianglesPickDepthRenderer { constructor(scene) { this._scene = scene; this._hash = this._getHash(); this._allocate(); } getValid() { return this._hash === this._getHash(); } _getHash() { return this._scene._sectionPlanesState.getHash(); } drawLayer(frameCtx, dataTextureLayer, renderPass) { const model = dataTextureLayer.model; const scene = model.scene; const camera = scene.camera; const gl = scene.canvas.gl; const state = dataTextureLayer._state; const textureState = state.textureState; const origin = dataTextureLayer._state.origin; const {position, rotationMatrix} = model; const viewMatrix = frameCtx.pickViewMatrix || camera.viewMatrix; const projMatrix = frameCtx.pickProjMatrix || camera.projMatrix; const eye = frameCtx.pickOrigin || camera.eye; const far = frameCtx.pickProjMatrix ? frameCtx.pickZFar : camera.project.far; if (!this._program) { this._allocate(); } if (frameCtx.lastProgramId !== this._program.id) { frameCtx.lastProgramId = this._program.id; this._bindProgram(); } textureState.bindCommonTextures( this._program, this.uTexturePerObjectPositionsDecodeMatrix, this._uTexturePerVertexIdCoordinates, this.uTexturePerObjectColorsAndFlags, this._uTexturePerObjectMatrix ); let rtcViewMatrix; let rtcCameraEye; if (origin || position[0] !== 0 || position[1] !== 0 || position[2] !== 0) { const rtcOrigin = tempVec3a$k; if (origin) { const rotatedOrigin = tempVec3b$g; math.transformPoint3(rotationMatrix, origin, rotatedOrigin); rtcOrigin[0] = rotatedOrigin[0]; rtcOrigin[1] = rotatedOrigin[1]; rtcOrigin[2] = rotatedOrigin[2]; } else { rtcOrigin[0] = 0; rtcOrigin[1] = 0; rtcOrigin[2] = 0; } rtcOrigin[0] += position[0]; rtcOrigin[1] += position[1]; rtcOrigin[2] += position[2]; rtcViewMatrix = createRTCViewMat(viewMatrix, rtcOrigin, tempMat4a$7); rtcCameraEye = tempVec3c$e; rtcCameraEye[0] = eye[0] - rtcOrigin[0]; rtcCameraEye[1] = eye[1] - rtcOrigin[1]; rtcCameraEye[2] = eye[2] - rtcOrigin[2]; frameCtx.snapPickOrigin[0] = rtcOrigin[0]; frameCtx.snapPickOrigin[1] = rtcOrigin[1]; frameCtx.snapPickOrigin[2] = rtcOrigin[2]; } else { rtcViewMatrix = viewMatrix; rtcCameraEye = eye; frameCtx.snapPickOrigin[0] = 0; frameCtx.snapPickOrigin[1] = 0; frameCtx.snapPickOrigin[2] = 0; } gl.uniform3fv(this._uCameraEyeRtc, rtcCameraEye); gl.uniform1i(this._uRenderPass, renderPass); gl.uniform1i(this._uPickInvisible, frameCtx.pickInvisible); gl.uniform2fv(this._uPickClipPos, frameCtx.pickClipPos); gl.uniform2f(this._uDrawingBufferSize, gl.drawingBufferWidth, gl.drawingBufferHeight); gl.uniform1f(this._uPickZNear, frameCtx.pickZNear); gl.uniform1f(this._uPickZFar, frameCtx.pickZFar); gl.uniformMatrix4fv(this._uSceneModelMatrix, false, rotationMatrix); gl.uniformMatrix4fv(this._uViewMatrix, false, rtcViewMatrix); gl.uniformMatrix4fv(this._uProjMatrix, false, projMatrix); if (scene.logarithmicDepthBufferEnabled) { const logDepthBufFC = 2.0 / (Math.log(far + 1.0) / Math.LN2); gl.uniform1f(this._uLogDepthBufFC, logDepthBufFC); } const numAllocatedSectionPlanes = scene._sectionPlanesState.getNumAllocatedSectionPlanes(); const numSectionPlanes = scene._sectionPlanesState.sectionPlanes.length; if (numAllocatedSectionPlanes > 0) { const sectionPlanes = scene._sectionPlanesState.sectionPlanes; const baseIndex = dataTextureLayer.layerIndex * numSectionPlanes; const renderFlags = model.renderFlags; for (let sectionPlaneIndex = 0; sectionPlaneIndex < numAllocatedSectionPlanes; sectionPlaneIndex++) { const sectionPlaneUniforms = this._uSectionPlanes[sectionPlaneIndex]; if (sectionPlaneUniforms) { if (sectionPlaneIndex < numSectionPlanes) { const active = renderFlags.sectionPlanesActivePerLayer[baseIndex + sectionPlaneIndex]; gl.uniform1i(sectionPlaneUniforms.active, active ? 1 : 0); if (active) { const sectionPlane = sectionPlanes[sectionPlaneIndex]; if (origin) { const rtcSectionPlanePos = getPlaneRTCPos(sectionPlane.dist, sectionPlane.dir, origin, tempVec3a$k, model.matrix); gl.uniform3fv(sectionPlaneUniforms.pos, rtcSectionPlanePos); } else { gl.uniform3fv(sectionPlaneUniforms.pos, sectionPlane.pos); } gl.uniform3fv(sectionPlaneUniforms.dir, sectionPlane.dir); } } else { gl.uniform1i(sectionPlaneUniforms.active, 0); } } } } if (state.numIndices8Bits > 0) { textureState.bindTriangleIndicesTextures( this._program, this._uTexturePerPolygonIdPortionIds, this._uTexturePerPolygonIdIndices, 8 // 8 bits indices ); gl.drawArrays(gl.TRIANGLES, 0, state.numIndices8Bits); } if (state.numIndices16Bits > 0) { textureState.bindTriangleIndicesTextures( this._program, this._uTexturePerPolygonIdPortionIds, this._uTexturePerPolygonIdIndices, 16 // 16 bits indices ); gl.drawArrays(gl.TRIANGLES, 0, state.numIndices16Bits); } if (state.numIndices32Bits > 0) { textureState.bindTriangleIndicesTextures( this._program, this._uTexturePerPolygonIdPortionIds, this._uTexturePerPolygonIdIndices, 32 // 32 bits indices ); gl.drawArrays(gl.TRIANGLES, 0, state.numIndices32Bits); } frameCtx.drawElements++; } _allocate() { const scene = this._scene; const gl = scene.canvas.gl; this._program = new Program(gl, this._buildShader()); if (this._program.errors) { this.errors = this._program.errors; return; } const program = this._program; this._uRenderPass = program.getLocation("renderPass"); this._uPickInvisible = program.getLocation("pickInvisible"); this._uPickClipPos = program.getLocation("pickClipPos"); this._uDrawingBufferSize = program.getLocation("drawingBufferSize"); this._uSceneModelMatrix = program.getLocation("sceneModelMatrix"); this._uViewMatrix = program.getLocation("viewMatrix"); this._uProjMatrix = program.getLocation("projMatrix"); this._uSectionPlanes = []; for (let i = 0, len = scene._sectionPlanesState.getNumAllocatedSectionPlanes(); i < len; i++) { this._uSectionPlanes.push({ active: program.getLocation("sectionPlaneActive" + i), pos: program.getLocation("sectionPlanePos" + i), dir: program.getLocation("sectionPlaneDir" + i) }); } this._uPickZNear = program.getLocation("pickZNear"); this._uPickZFar = program.getLocation("pickZFar"); if (scene.logarithmicDepthBufferEnabled) { this._uLogDepthBufFC = program.getLocation("logDepthBufFC"); } this.uTexturePerObjectPositionsDecodeMatrix = "uObjectPerObjectPositionsDecodeMatrix"; this.uTexturePerObjectColorsAndFlags = "uObjectPerObjectColorsAndFlags"; this._uTexturePerVertexIdCoordinates = "uTexturePerVertexIdCoordinates"; this._uTexturePerPolygonIdNormals = "uTexturePerPolygonIdNormals"; this._uTexturePerPolygonIdIndices = "uTexturePerPolygonIdIndices"; this._uTexturePerPolygonIdPortionIds = "uTexturePerPolygonIdPortionIds"; this._uTexturePerObjectMatrix= "uTexturePerObjectMatrix"; this._uCameraEyeRtc = program.getLocation("uCameraEyeRtc"); } _bindProgram() { this._program.bind(); } _buildShader() { return { vertex: this._buildVertexShader(), fragment: this._buildFragmentShader() }; } _buildVertexShader() { const scene = this._scene; const clipping = scene._sectionPlanesState.getNumAllocatedSectionPlanes() > 0; const src = []; src.push("#version 300 es"); src.push("// Triangles dataTexture pick depth vertex shader"); src.push("#ifdef GL_FRAGMENT_PRECISION_HIGH"); src.push("precision highp float;"); src.push("precision highp int;"); src.push("precision highp usampler2D;"); src.push("precision highp isampler2D;"); src.push("precision highp sampler2D;"); src.push("#else"); src.push("precision mediump float;"); src.push("precision mediump int;"); src.push("precision mediump usampler2D;"); src.push("precision mediump isampler2D;"); src.push("precision mediump sampler2D;"); src.push("#endif"); src.push("uniform int renderPass;"); if (scene.entityOffsetsEnabled) { src.push("in vec3 offset;"); } src.push("uniform mat4 sceneModelMatrix;"); src.push("uniform mat4 viewMatrix;"); src.push("uniform mat4 projMatrix;"); src.push("uniform bool pickInvisible;"); src.push("uniform highp sampler2D uObjectPerObjectPositionsDecodeMatrix;"); src.push("uniform highp sampler2D uTexturePerObjectMatrix;"); src.push("uniform lowp usampler2D uObjectPerObjectColorsAndFlags;"); src.push("uniform mediump usampler2D uTexturePerVertexIdCoordinates;"); src.push("uniform highp usampler2D uTexturePerPolygonIdIndices;"); src.push("uniform mediump usampler2D uTexturePerPolygonIdPortionIds;"); src.push("uniform vec3 uCameraEyeRtc;"); src.push("vec3 positions[3];"); if (scene.logarithmicDepthBufferEnabled) { src.push("uniform float logDepthBufFC;"); src.push("out float vFragDepth;"); src.push("out float isPerspective;"); } src.push("uniform vec2 pickClipPos;"); src.push("uniform vec2 drawingBufferSize;"); src.push("vec4 remapClipPos(vec4 clipPos) {"); src.push(" clipPos.xy /= clipPos.w;"); src.push(` clipPos.xy = (clipPos.xy - pickClipPos) * drawingBufferSize;`); src.push(" clipPos.xy *= clipPos.w;"); src.push(" return clipPos;"); src.push("}"); src.push("bool isPerspectiveMatrix(mat4 m) {"); src.push(" return (m[2][3] == - 1.0);"); src.push("}"); if (clipping) { src.push("out vec4 vWorldPosition;"); src.push("flat out uint vFlags2;"); } src.push("out vec4 vViewPosition;"); src.push("void main(void) {"); // constants src.push("int polygonIndex = gl_VertexID / 3;"); // get packed object-id src.push("int h_packed_object_id_index = (polygonIndex >> 3) & 4095;"); src.push("int v_packed_object_id_index = (polygonIndex >> 3) >> 12;"); src.push("int objectIndex = int(texelFetch(uTexturePerPolygonIdPortionIds, ivec2(h_packed_object_id_index, v_packed_object_id_index), 0).r);"); src.push("ivec2 objectIndexCoords = ivec2(objectIndex % 512, objectIndex / 512);"); // get flags & flags2 src.push("uvec4 flags = texelFetch (uObjectPerObjectColorsAndFlags, ivec2(objectIndexCoords.x*8+2, objectIndexCoords.y), 0);"); src.push("uvec4 flags2 = texelFetch (uObjectPerObjectColorsAndFlags, ivec2(objectIndexCoords.x*8+3, objectIndexCoords.y), 0);"); // flags.w = NOT_RENDERED | PICK // renderPass = PICK src.push(`if (int(flags.w) != renderPass) {`); src.push(" gl_Position = vec4(3.0, 3.0, 3.0, 1.0);"); // Cull vertex src.push(" return;"); // Cull vertex src.push("} else {"); // get vertex base src.push("ivec4 packedVertexBase = ivec4(texelFetch (uObjectPerObjectColorsAndFlags, ivec2(objectIndexCoords.x*8+4, objectIndexCoords.y), 0));"); src.push("ivec4 packedIndexBaseOffset = ivec4(texelFetch (uObjectPerObjectColorsAndFlags, ivec2(objectIndexCoords.x*8+5, objectIndexCoords.y), 0));"); src.push("int indexBaseOffset = (packedIndexBaseOffset.r << 24) + (packedIndexBaseOffset.g << 16) + (packedIndexBaseOffset.b << 8) + packedIndexBaseOffset.a;"); src.push("int h_index = (polygonIndex - indexBaseOffset) & 4095;"); src.push("int v_index = (polygonIndex - indexBaseOffset) >> 12;"); src.push("ivec3 vertexIndices = ivec3(texelFetch(uTexturePerPolygonIdIndices, ivec2(h_index, v_index), 0));"); src.push("ivec3 uniqueVertexIndexes = vertexIndices + (packedVertexBase.r << 24) + (packedVertexBase.g << 16) + (packedVertexBase.b << 8) + packedVertexBase.a;"); src.push("ivec3 indexPositionH = uniqueVertexIndexes & 4095;"); src.push("ivec3 indexPositionV = uniqueVertexIndexes >> 12;"); src.push("mat4 objectInstanceMatrix = mat4 (texelFetch (uTexturePerObjectMatrix, ivec2(objectIndexCoords.x*4+0, objectIndexCoords.y), 0), texelFetch (uTexturePerObjectMatrix, ivec2(objectIndexCoords.x*4+1, objectIndexCoords.y), 0), texelFetch (uTexturePerObjectMatrix, ivec2(objectIndexCoords.x*4+2, objectIndexCoords.y), 0), texelFetch (uTexturePerObjectMatrix, ivec2(objectIndexCoords.x*4+3, objectIndexCoords.y), 0));"); src.push("mat4 objectDecodeAndInstanceMatrix = objectInstanceMatrix * mat4 (texelFetch (uObjectPerObjectPositionsDecodeMatrix, ivec2(objectIndexCoords.x*4+0, objectIndexCoords.y), 0), texelFetch (uObjectPerObjectPositionsDecodeMatrix, ivec2(objectIndexCoords.x*4+1, objectIndexCoords.y), 0), texelFetch (uObjectPerObjectPositionsDecodeMatrix, ivec2(objectIndexCoords.x*4+2, objectIndexCoords.y), 0), texelFetch (uObjectPerObjectPositionsDecodeMatrix, ivec2(objectIndexCoords.x*4+3, objectIndexCoords.y), 0));"); src.push("uint solid = texelFetch (uObjectPerObjectColorsAndFlags, ivec2(objectIndexCoords.x*8+7, objectIndexCoords.y), 0).r;"); // get position src.push("positions[0] = vec3(texelFetch(uTexturePerVertexIdCoordinates, ivec2(indexPositionH.r, indexPositionV.r), 0));"); src.push("positions[1] = vec3(texelFetch(uTexturePerVertexIdCoordinates, ivec2(indexPositionH.g, indexPositionV.g), 0));"); src.push("positions[2] = vec3(texelFetch(uTexturePerVertexIdCoordinates, ivec2(indexPositionH.b, indexPositionV.b), 0));"); // get color src.push("uvec4 color = texelFetch (uObjectPerObjectColorsAndFlags, ivec2(objectIndexCoords.x*8+0, objectIndexCoords.y), 0);"); src.push(`if (color.a == 0u) {`); src.push(" gl_Position = vec4(3.0, 3.0, 3.0, 1.0);"); // Cull vertex src.push(" return;"); src.push("};"); // get normal src.push("vec3 normal = normalize(cross(positions[2] - positions[0], positions[1] - positions[0]));"); src.push("vec3 position;"); src.push("position = positions[gl_VertexID % 3];"); // when the geometry is not solid, if needed, flip the triangle winding src.push("if (solid != 1u) {"); src.push("if (isPerspectiveMatrix(projMatrix)) {"); src.push("vec3 uCameraEyeRtcInQuantizedSpace = (inverse(sceneModelMatrix * objectDecodeAndInstanceMatrix) * vec4(uCameraEyeRtc, 1)).xyz;"); src.push("if (dot(position.xyz - uCameraEyeRtcInQuantizedSpace, normal) < 0.0) {"); src.push("position = positions[2 - (gl_VertexID % 3)];"); src.push("}"); src.push("} else {"); src.push("vec3 viewNormal = -normalize((transpose(inverse(viewMatrix*objectDecodeAndInstanceMatrix)) * vec4(normal,1)).xyz);"); src.push("if (viewNormal.z < 0.0) {"); src.push("position = positions[2 - (gl_VertexID % 3)];"); src.push("}"); src.push("}"); src.push("}"); src.push("vec4 worldPosition = sceneModelMatrix * (objectDecodeAndInstanceMatrix * vec4(position, 1.0)); "); src.push("vec4 viewPosition = viewMatrix * worldPosition; "); if (clipping) { src.push(" vWorldPosition = worldPosition;"); src.push(" vFlags2 = flags2.r;"); } src.push("vViewPosition = viewPosition;"); src.push("vec4 clipPos = projMatrix * viewPosition;"); if (scene.logarithmicDepthBufferEnabled) { src.push("vFragDepth = 1.0 + clipPos.w;"); src.push("isPerspective = float (isPerspectiveMatrix(projMatrix));"); } src.push("gl_Position = remapClipPos(clipPos);"); src.push(" }"); src.push("}"); return src; } _buildFragmentShader() { const scene = this._scene; const clipping = scene._sectionPlanesState.getNumAllocatedSectionPlanes() > 0; const src = []; src.push('#version 300 es'); src.push("// Triangles dataTexture pick depth fragment shader"); src.push("#ifdef GL_FRAGMENT_PRECISION_HIGH"); src.push("precision highp float;"); src.push("precision highp int;"); src.push("#else"); src.push("precision mediump float;"); src.push("precision mediump int;"); src.push("#endif"); if (scene.logarithmicDepthBufferEnabled) { src.push("in float isPerspective;"); src.push("uniform float logDepthBufFC;"); src.push("in float vFragDepth;"); } src.push("uniform float pickZNear;"); src.push("uniform float pickZFar;"); if (clipping) { src.push("in vec4 vWorldPosition;"); src.push("flat in uint vFlags2;"); for (var i = 0; i < scene._sectionPlanesState.getNumAllocatedSectionPlanes(); i++) { src.push("uniform bool sectionPlaneActive" + i + ";"); src.push("uniform vec3 sectionPlanePos" + i + ";"); src.push("uniform vec3 sectionPlaneDir" + i + ";"); } } src.push("in vec4 vViewPosition;"); src.push("vec4 packDepth(const in float depth) {"); src.push(" const vec4 bitShift = vec4(256.0*256.0*256.0, 256.0*256.0, 256.0, 1.0);"); src.push(" const vec4 bitMask = vec4(0.0, 1.0/256.0, 1.0/256.0, 1.0/256.0);"); src.push(" vec4 res = fract(depth * bitShift);"); src.push(" res -= res.xxyz * bitMask;"); src.push(" return res;"); src.push("}"); src.push("out vec4 outPackedDepth;"); src.push("void main(void) {"); if (clipping) { src.push(" bool clippable = vFlags2 > 0u;"); src.push(" if (clippable) {"); src.push(" float dist = 0.0;"); for (var i = 0; i < scene._sectionPlanesState.getNumAllocatedSectionPlanes(); i++) { src.push(" if (sectionPlaneActive" + i + ") {"); src.push(" dist += clamp(dot(-sectionPlaneDir" + i + ".xyz, vWorldPosition.xyz - sectionPlanePos" + i + ".xyz), 0.0, 1000.0);"); src.push(" }"); } src.push(" if (dist > 0.0) { discard; }"); src.push(" }"); } if (scene.logarithmicDepthBufferEnabled) { src.push(" gl_FragDepth = isPerspective == 0.0 ? gl_FragCoord.z : log2( vFragDepth ) * logDepthBufFC * 0.5;"); } src.push(" float zNormalizedDepth = abs((pickZNear + vViewPosition.z) / (pickZFar - pickZNear));"); src.push(" outPackedDepth = packDepth(zNormalizedDepth); "); // Must be linear depth src.push("}"); return src; } webglContextRestored() { this._program = null; } destroy() { if (this._program) { this._program.destroy(); } this._program = null; } } const tempVec3a$j = math.vec3(); const tempVec3b$f = math.vec3(); const tempVec3c$d = math.vec3(); const tempVec3d$4 = math.vec3(); math.vec3(); const tempMat4a$6 = math.mat4(); /** * @private */ class DTXTrianglesSnapRenderer { constructor(scene) { this._scene = scene; this._hash = this._getHash(); this._allocate(); } getValid() { return this._hash === this._getHash(); }; _getHash() { return this._scene._sectionPlanesState.getHash(); } drawLayer(frameCtx, dataTextureLayer, renderPass) { if (!this._program) { this._allocate(); if (this.errors) { return; } } if (frameCtx.lastProgramId !== this._program.id) { frameCtx.lastProgramId = this._program.id; this._bindProgram(); } const model = dataTextureLayer.model; const scene = model.scene; const camera = scene.camera; const gl = scene.canvas.gl; const state = dataTextureLayer._state; const textureState = state.textureState; const origin = dataTextureLayer._state.origin; const {position, rotationMatrix} = model; const aabb = dataTextureLayer.aabb; // Per-layer AABB for best RTC accuracy const viewMatrix = frameCtx.pickViewMatrix || camera.viewMatrix; const coordinateScaler = tempVec3a$j; coordinateScaler[0] = math.safeInv(aabb[3] - aabb[0]) * math.MAX_INT; coordinateScaler[1] = math.safeInv(aabb[4] - aabb[1]) * math.MAX_INT; coordinateScaler[2] = math.safeInv(aabb[5] - aabb[2]) * math.MAX_INT; frameCtx.snapPickCoordinateScale[0] = math.safeInv(coordinateScaler[0]); frameCtx.snapPickCoordinateScale[1] = math.safeInv(coordinateScaler[1]); frameCtx.snapPickCoordinateScale[2] = math.safeInv(coordinateScaler[2]); textureState.bindCommonTextures( this._program, this.uTexturePerObjectPositionsDecodeMatrix, this._uTexturePerVertexIdCoordinates, this.uTexturePerObjectColorsAndFlags, this._uTexturePerObjectMatrix ); let rtcViewMatrix; let rtcCameraEye; const gotOrigin = (origin[0] !== 0 || origin[1] !== 0 || origin[2] !== 0); const gotPosition = (position[0] !== 0 || position[1] !== 0 || position[2] !== 0); if (gotOrigin || gotPosition) { const rtcOrigin = tempVec3b$f; if (gotOrigin) { const rotatedOrigin = math.transformPoint3(rotationMatrix, origin, tempVec3c$d); rtcOrigin[0] = rotatedOrigin[0]; rtcOrigin[1] = rotatedOrigin[1]; rtcOrigin[2] = rotatedOrigin[2]; } else { rtcOrigin[0] = 0; rtcOrigin[1] = 0; rtcOrigin[2] = 0; } rtcOrigin[0] += position[0]; rtcOrigin[1] += position[1]; rtcOrigin[2] += position[2]; rtcViewMatrix = createRTCViewMat(viewMatrix, rtcOrigin, tempMat4a$6); rtcCameraEye = tempVec3d$4; rtcCameraEye[0] = camera.eye[0] - rtcOrigin[0]; rtcCameraEye[1] = camera.eye[1] - rtcOrigin[1]; rtcCameraEye[2] = camera.eye[2] - rtcOrigin[2]; frameCtx.snapPickOrigin[0] = rtcOrigin[0]; frameCtx.snapPickOrigin[1] = rtcOrigin[1]; frameCtx.snapPickOrigin[2] = rtcOrigin[2]; } else { rtcViewMatrix = viewMatrix; rtcCameraEye = camera.eye; frameCtx.snapPickOrigin[0] = 0; frameCtx.snapPickOrigin[1] = 0; frameCtx.snapPickOrigin[2] = 0; } gl.uniform3fv(this._uCameraEyeRtc, rtcCameraEye); gl.uniform2fv(this.uVectorA, frameCtx.snapVectorA); gl.uniform2fv(this.uInverseVectorAB, frameCtx.snapInvVectorAB); gl.uniform1i(this._uLayerNumber, frameCtx.snapPickLayerNumber); gl.uniform3fv(this._uCoordinateScaler, coordinateScaler); gl.uniform1i(this._uRenderPass, renderPass); gl.uniform1i(this._uPickInvisible, frameCtx.pickInvisible); gl.uniformMatrix4fv(this._uSceneModelMatrix, false, rotationMatrix); gl.uniformMatrix4fv(this._uViewMatrix, false, rtcViewMatrix); gl.uniformMatrix4fv(this._uProjMatrix, false, camera.projMatrix); { const logDepthBufFC = 2.0 / (Math.log(frameCtx.pickZFar + 1.0) / Math.LN2); gl.uniform1f(this._uLogDepthBufFC, logDepthBufFC); } const numAllocatedSectionPlanes = scene._sectionPlanesState.getNumAllocatedSectionPlanes(); const numSectionPlanes = scene._sectionPlanesState.sectionPlanes.length; if (numAllocatedSectionPlanes > 0) { const sectionPlanes = scene._sectionPlanesState.sectionPlanes; const baseIndex = dataTextureLayer.layerIndex * numSectionPlanes; const renderFlags = model.renderFlags; for (let sectionPlaneIndex = 0; sectionPlaneIndex < numAllocatedSectionPlanes; sectionPlaneIndex++) { const sectionPlaneUniforms = this._uSectionPlanes[sectionPlaneIndex]; if (sectionPlaneUniforms) { if (sectionPlaneIndex < numSectionPlanes) { const active = renderFlags.sectionPlanesActivePerLayer[baseIndex + sectionPlaneIndex]; gl.uniform1i(sectionPlaneUniforms.active, active ? 1 : 0); if (active) { const sectionPlane = sectionPlanes[sectionPlaneIndex]; if (origin) { const rtcSectionPlanePos = getPlaneRTCPos(sectionPlane.dist, sectionPlane.dir, origin, tempVec3a$j, model.matrix); gl.uniform3fv(sectionPlaneUniforms.pos, rtcSectionPlanePos); } else { gl.uniform3fv(sectionPlaneUniforms.pos, sectionPlane.pos); } gl.uniform3fv(sectionPlaneUniforms.dir, sectionPlane.dir); } } else { gl.uniform1i(sectionPlaneUniforms.active, 0); } } } } const glMode = (frameCtx.snapMode === "edge") ? gl.LINES : gl.POINTS; if (state.numEdgeIndices8Bits > 0) { textureState.bindEdgeIndicesTextures( this._program, this._uTexturePerEdgeIdPortionIds, this._uTexturePerPolygonIdEdgeIndices, 8 // 8 bits edge indices ); gl.drawArrays(glMode, 0, state.numEdgeIndices8Bits); } if (state.numEdgeIndices16Bits > 0) { textureState.bindEdgeIndicesTextures( this._program, this._uTexturePerEdgeIdPortionIds, this._uTexturePerPolygonIdEdgeIndices, 16 // 16 bits edge indices ); gl.drawArrays(glMode, 0, state.numEdgeIndices16Bits); } if (state.numEdgeIndices32Bits > 0) { textureState.bindEdgeIndicesTextures( this._program, this._uTexturePerEdgeIdPortionIds, this._uTexturePerPolygonIdEdgeIndices, 32 // 32 bits edge indices ); gl.drawArrays(glMode, 0, state.numEdgeIndices32Bits); } frameCtx.drawElements++; } _allocate() { const scene = this._scene; const gl = scene.canvas.gl; this._program = new Program(gl, this._buildShader()); if (this._program.errors) { this.errors = this._program.errors; return; } const program = this._program; this._uRenderPass = program.getLocation("renderPass"); this._uPickInvisible = program.getLocation("pickInvisible"); this._uSceneModelMatrix = program.getLocation("sceneModelMatrix"); this._uViewMatrix = program.getLocation("viewMatrix"); this._uProjMatrix = program.getLocation("projMatrix"); this._uSectionPlanes = []; for (let i = 0, len = scene._sectionPlanesState.getNumAllocatedSectionPlanes(); i < len; i++) { this._uSectionPlanes.push({ active: program.getLocation("sectionPlaneActive" + i), pos: program.getLocation("sectionPlanePos" + i), dir: program.getLocation("sectionPlaneDir" + i) }); } { this._uLogDepthBufFC = program.getLocation("logDepthBufFC"); } this.uTexturePerObjectPositionsDecodeMatrix = "uObjectPerObjectPositionsDecodeMatrix"; this.uTexturePerObjectColorsAndFlags = "uObjectPerObjectColorsAndFlags"; this._uTexturePerVertexIdCoordinates = "uTexturePerVertexIdCoordinates"; this._uTexturePerPolygonIdEdgeIndices = "uTexturePerPolygonIdEdgeIndices"; this._uTexturePerEdgeIdPortionIds = "uTexturePerEdgeIdPortionIds"; this._uTextureModelMatrices = "uTextureModelMatrices"; this._uTexturePerObjectMatrix= "uTexturePerObjectMatrix"; this._uCameraEyeRtc = program.getLocation("uCameraEyeRtc"); this.uVectorA = program.getLocation("uSnapVectorA"); this.uInverseVectorAB = program.getLocation("uSnapInvVectorAB"); this._uLayerNumber = program.getLocation("uLayerNumber"); this._uCoordinateScaler = program.getLocation("uCoordinateScaler"); } _bindProgram() { this._program.bind(); } _buildShader() { return { vertex: this._buildVertexShader(), fragment: this._buildFragmentShader() }; } _buildVertexShader() { const scene = this._scene; const clipping = scene._sectionPlanesState.getNumAllocatedSectionPlanes() > 0; const src = []; src.push("#version 300 es"); src.push("// Batched geometry edges drawing vertex shader"); src.push("#ifdef GL_FRAGMENT_PRECISION_HIGH"); src.push("precision highp float;"); src.push("precision highp int;"); src.push("precision highp usampler2D;"); src.push("precision highp isampler2D;"); src.push("precision highp sampler2D;"); src.push("#else"); src.push("precision mediump float;"); src.push("precision mediump int;"); src.push("precision mediump usampler2D;"); src.push("precision mediump isampler2D;"); src.push("precision mediump sampler2D;"); src.push("#endif"); src.push("uniform int renderPass;"); if (scene.entityOffsetsEnabled) { src.push("in vec3 offset;"); } src.push("uniform mat4 sceneModelMatrix;"); src.push("uniform mat4 viewMatrix;"); src.push("uniform mat4 projMatrix;"); src.push("uniform highp sampler2D uObjectPerObjectPositionsDecodeMatrix;"); src.push("uniform lowp usampler2D uObjectPerObjectColorsAndFlags;"); src.push("uniform highp sampler2D uTexturePerObjectMatrix;"); src.push("uniform mediump usampler2D uTexturePerVertexIdCoordinates;"); src.push("uniform highp usampler2D uTexturePerPolygonIdEdgeIndices;"); src.push("uniform mediump usampler2D uTexturePerEdgeIdPortionIds;"); src.push("uniform vec3 uCameraEyeRtc;"); src.push("uniform vec2 uSnapVectorA;"); src.push("uniform vec2 uSnapInvVectorAB;"); src.push("vec3 positions[3];"); { src.push("uniform float logDepthBufFC;"); src.push("out float vFragDepth;"); src.push("bool isPerspectiveMatrix(mat4 m) {"); src.push(" return (m[2][3] == - 1.0);"); src.push("}"); src.push("out float isPerspective;"); } src.push("vec2 remapClipPos(vec2 clipPos) {"); src.push(" float x = (clipPos.x - uSnapVectorA.x) * uSnapInvVectorAB.x;"); src.push(" float y = (clipPos.y - uSnapVectorA.y) * uSnapInvVectorAB.y;"); src.push(" return vec2(x, y);"); src.push("}"); if (clipping) { src.push("out vec4 vWorldPosition;"); src.push("flat out uint vFlags2;"); } src.push("out vec4 vViewPosition;"); src.push("out highp vec3 relativeToOriginPosition;"); src.push("void main(void) {"); // constants src.push("int edgeIndex = gl_VertexID / 2;"); // get packed object-id src.push("int h_packed_object_id_index = (edgeIndex >> 3) & 4095;"); src.push("int v_packed_object_id_index = (edgeIndex >> 3) >> 12;"); src.push("int objectIndex = int(texelFetch(uTexturePerEdgeIdPortionIds, ivec2(h_packed_object_id_index, v_packed_object_id_index), 0).r);"); src.push("ivec2 objectIndexCoords = ivec2(objectIndex % 512, objectIndex / 512);"); // get flags & flags2 src.push("uvec4 flags = texelFetch (uObjectPerObjectColorsAndFlags, ivec2(objectIndexCoords.x*8+2, objectIndexCoords.y), 0);"); src.push("uvec4 flags2 = texelFetch (uObjectPerObjectColorsAndFlags, ivec2(objectIndexCoords.x*8+3, objectIndexCoords.y), 0);"); // flags.w = NOT_RENDERED | PICK // renderPass = PICK src.push(`if (int(flags.w) != renderPass) {`); src.push(" gl_Position = vec4(3.0, 3.0, 3.0, 1.0);"); // Cull vertex src.push(" return;"); // Cull vertex src.push("}"); src.push("{"); // get vertex base src.push("ivec4 packedVertexBase = ivec4(texelFetch (uObjectPerObjectColorsAndFlags, ivec2(objectIndexCoords.x*8+4, objectIndexCoords.y), 0));"); src.push("ivec4 packedEdgeIndexBaseOffset = ivec4(texelFetch (uObjectPerObjectColorsAndFlags, ivec2(objectIndexCoords.x*8+6, objectIndexCoords.y), 0));"); src.push("int edgeIndexBaseOffset = (packedEdgeIndexBaseOffset.r << 24) + (packedEdgeIndexBaseOffset.g << 16) + (packedEdgeIndexBaseOffset.b << 8) + packedEdgeIndexBaseOffset.a;"); src.push("int h_index = (edgeIndex - edgeIndexBaseOffset) & 4095;"); src.push("int v_index = (edgeIndex - edgeIndexBaseOffset) >> 12;"); src.push("ivec3 vertexIndices = ivec3(texelFetch(uTexturePerPolygonIdEdgeIndices, ivec2(h_index, v_index), 0));"); src.push("ivec3 uniqueVertexIndexes = vertexIndices + (packedVertexBase.r << 24) + (packedVertexBase.g << 16) + (packedVertexBase.b << 8) + packedVertexBase.a;"); src.push("int indexPositionH = uniqueVertexIndexes[gl_VertexID % 2] & 4095;"); src.push("int indexPositionV = uniqueVertexIndexes[gl_VertexID % 2] >> 12;"); src.push("mat4 objectInstanceMatrix = mat4 (texelFetch (uTexturePerObjectMatrix, ivec2(objectIndexCoords.x*4+0, objectIndexCoords.y), 0), texelFetch (uTexturePerObjectMatrix, ivec2(objectIndexCoords.x*4+1, objectIndexCoords.y), 0), texelFetch (uTexturePerObjectMatrix, ivec2(objectIndexCoords.x*4+2, objectIndexCoords.y), 0), texelFetch (uTexturePerObjectMatrix, ivec2(objectIndexCoords.x*4+3, objectIndexCoords.y), 0));"); src.push("mat4 objectDecodeAndInstanceMatrix = objectInstanceMatrix * mat4 (texelFetch (uObjectPerObjectPositionsDecodeMatrix, ivec2(objectIndexCoords.x*4+0, objectIndexCoords.y), 0), texelFetch (uObjectPerObjectPositionsDecodeMatrix, ivec2(objectIndexCoords.x*4+1, objectIndexCoords.y), 0), texelFetch (uObjectPerObjectPositionsDecodeMatrix, ivec2(objectIndexCoords.x*4+2, objectIndexCoords.y), 0), texelFetch (uObjectPerObjectPositionsDecodeMatrix, ivec2(objectIndexCoords.x*4+3, objectIndexCoords.y), 0));"); src.push("uvec4 flags = texelFetch (uObjectPerObjectColorsAndFlags, ivec2(objectIndexCoords.x*8+2, objectIndexCoords.y), 0);"); src.push("uvec4 flags2 = texelFetch (uObjectPerObjectColorsAndFlags, ivec2(objectIndexCoords.x*8+3, objectIndexCoords.y), 0);"); src.push("vec3 position = vec3(texelFetch(uTexturePerVertexIdCoordinates, ivec2(indexPositionH, indexPositionV), 0));"); src.push("vec4 worldPosition = sceneModelMatrix * (objectDecodeAndInstanceMatrix * vec4(position, 1.0)); "); src.push("relativeToOriginPosition = worldPosition.xyz;"); src.push(" vec4 viewPosition = viewMatrix * worldPosition; "); if (clipping) { src.push(" vWorldPosition = worldPosition;"); src.push(" vFlags2 = flags2.r;"); } src.push("vViewPosition = viewPosition;"); src.push("vec4 clipPos = projMatrix * viewPosition;"); src.push("float tmp = clipPos.w;"); src.push("clipPos.xyzw /= tmp;"); src.push("clipPos.xy = remapClipPos(clipPos.xy);"); src.push("clipPos.xyzw *= tmp;"); src.push("vViewPosition = clipPos;"); { src.push("vFragDepth = 1.0 + clipPos.w;"); src.push("isPerspective = float (isPerspectiveMatrix(projMatrix));"); } src.push("gl_Position = clipPos;"); src.push("gl_PointSize = 1.0;"); // Windows needs this? src.push(" }"); src.push("}"); return src; } _buildFragmentShader() { const scene = this._scene; const clipping = scene._sectionPlanesState.getNumAllocatedSectionPlanes() > 0; const src = []; src.push('#version 300 es'); src.push("// Triangles dataTexture pick depth fragment shader"); src.push("#ifdef GL_FRAGMENT_PRECISION_HIGH"); src.push("precision highp float;"); src.push("precision highp int;"); src.push("#else"); src.push("precision mediump float;"); src.push("precision mediump int;"); src.push("#endif"); { src.push("in float isPerspective;"); src.push("uniform float logDepthBufFC;"); src.push("in float vFragDepth;"); } src.push("uniform int uLayerNumber;"); src.push("uniform vec3 uCoordinateScaler;"); if (clipping) { src.push("in vec4 vWorldPosition;"); src.push("flat in uint vFlags2;"); for (let i = 0, len = scene._sectionPlanesState.getNumAllocatedSectionPlanes(); i < len; i++) { src.push("uniform bool sectionPlaneActive" + i + ";"); src.push("uniform vec3 sectionPlanePos" + i + ";"); src.push("uniform vec3 sectionPlaneDir" + i + ";"); } } src.push("in highp vec3 relativeToOriginPosition;"); src.push("out highp ivec4 outCoords;"); src.push("void main(void) {"); if (clipping) { src.push(" bool clippable = vFlags2 > 0u;"); src.push(" if (clippable) {"); src.push(" float dist = 0.0;"); for (var i = 0; i < scene._sectionPlanesState.getNumAllocatedSectionPlanes(); i++) { src.push(" if (sectionPlaneActive" + i + ") {"); src.push(" dist += clamp(dot(-sectionPlaneDir" + i + ".xyz, vWorldPosition.xyz - sectionPlanePos" + i + ".xyz), 0.0, 1000.0);"); src.push(" }"); } src.push(" if (dist > 0.0) { discard; }"); src.push(" }"); } { src.push(" gl_FragDepth = isPerspective == 0.0 ? gl_FragCoord.z : log2( vFragDepth ) * logDepthBufFC * 0.5;"); } src.push("outCoords = ivec4(relativeToOriginPosition.xyz * uCoordinateScaler.xyz, uLayerNumber);"); src.push("}"); return src; } webglContextRestored() { this._program = null; } destroy() { if (this._program) { this._program.destroy(); } this._program = null; } } const tempVec3a$i = math.vec3(); const tempVec3b$e = math.vec3(); const tempVec3c$c = math.vec3(); const tempVec3d$3 = math.vec3(); math.vec3(); const tempMat4a$5 = math.mat4(); /** * @private */ class DTXTrianglesSnapInitRenderer { constructor(scene) { this._scene = scene; this._hash = this._getHash(); this._allocate(); } getValid() { return this._hash === this._getHash(); }; _getHash() { return this._scene._sectionPlanesState.getHash(); } drawLayer(frameCtx, dataTextureLayer, renderPass) { if (!this._program) { this._allocate(); } if (frameCtx.lastProgramId !== this._program.id) { frameCtx.lastProgramId = this._program.id; this._bindProgram(); } const model = dataTextureLayer.model; const scene = model.scene; const camera = scene.camera; const gl = scene.canvas.gl; const state = dataTextureLayer._state; const textureState = state.textureState; const origin = dataTextureLayer._state.origin; const {position, rotationMatrix} = model; const aabb = dataTextureLayer.aabb; // Per-layer AABB for best RTC accuracy const viewMatrix = frameCtx.pickViewMatrix || camera.viewMatrix; const coordinateScaler = tempVec3a$i; coordinateScaler[0] = math.safeInv(aabb[3] - aabb[0]) * math.MAX_INT; coordinateScaler[1] = math.safeInv(aabb[4] - aabb[1]) * math.MAX_INT; coordinateScaler[2] = math.safeInv(aabb[5] - aabb[2]) * math.MAX_INT; frameCtx.snapPickCoordinateScale[0] = math.safeInv(coordinateScaler[0]); frameCtx.snapPickCoordinateScale[1] = math.safeInv(coordinateScaler[1]); frameCtx.snapPickCoordinateScale[2] = math.safeInv(coordinateScaler[2]); textureState.bindCommonTextures( this._program, this.uTexturePerObjectPositionsDecodeMatrix, this._uTexturePerVertexIdCoordinates, this.uTexturePerObjectColorsAndFlags, this._uTexturePerObjectMatrix ); let rtcViewMatrix; let rtcCameraEye; const gotOrigin = (origin[0] !== 0 || origin[1] !== 0 || origin[2] !== 0); const gotPosition = (position[0] !== 0 || position[1] !== 0 || position[2] !== 0); if (gotOrigin || gotPosition) { const rtcOrigin = tempVec3b$e; if (gotOrigin) { const rotatedOrigin = tempVec3c$c; math.transformPoint3(rotationMatrix, origin, rotatedOrigin); rtcOrigin[0] = rotatedOrigin[0]; rtcOrigin[1] = rotatedOrigin[1]; rtcOrigin[2] = rotatedOrigin[2]; } else { rtcOrigin[0] = 0; rtcOrigin[1] = 0; rtcOrigin[2] = 0; } rtcOrigin[0] += position[0]; rtcOrigin[1] += position[1]; rtcOrigin[2] += position[2]; rtcViewMatrix = createRTCViewMat(viewMatrix, rtcOrigin, tempMat4a$5); rtcCameraEye = tempVec3d$3; rtcCameraEye[0] = camera.eye[0] - rtcOrigin[0]; rtcCameraEye[1] = camera.eye[1] - rtcOrigin[1]; rtcCameraEye[2] = camera.eye[2] - rtcOrigin[2]; frameCtx.snapPickOrigin[0] = rtcOrigin[0]; frameCtx.snapPickOrigin[1] = rtcOrigin[1]; frameCtx.snapPickOrigin[2] = rtcOrigin[2]; } else { rtcViewMatrix = viewMatrix; rtcCameraEye = camera.eye; frameCtx.snapPickOrigin[0] = 0; frameCtx.snapPickOrigin[1] = 0; frameCtx.snapPickOrigin[2] = 0; } gl.uniform3fv(this._uCameraEyeRtc, rtcCameraEye); gl.uniform2fv(this._uVectorA, frameCtx.snapVectorA); gl.uniform2fv(this._uInverseVectorAB, frameCtx.snapInvVectorAB); gl.uniform1i(this._uLayerNumber, frameCtx.snapPickLayerNumber); gl.uniform3fv(this._uCoordinateScaler, coordinateScaler); gl.uniform1i(this._uRenderPass, renderPass); gl.uniform1i(this._uPickInvisible, frameCtx.pickInvisible); gl.uniformMatrix4fv(this._uSceneWorldModelMatrix, false, rotationMatrix); gl.uniformMatrix4fv(this._uViewMatrix, false, rtcViewMatrix); gl.uniformMatrix4fv(this._uProjMatrix, false, camera.projMatrix); { const logDepthBufFC = 2.0 / (Math.log(frameCtx.pickZFar + 1.0) / Math.LN2); gl.uniform1f(this._uLogDepthBufFC, logDepthBufFC); } const numAllocatedSectionPlanes = scene._sectionPlanesState.getNumAllocatedSectionPlanes(); const numSectionPlanes = scene._sectionPlanesState.sectionPlanes.length; if (numAllocatedSectionPlanes > 0) { const sectionPlanes = scene._sectionPlanesState.sectionPlanes; const baseIndex = dataTextureLayer.layerIndex * numSectionPlanes; const renderFlags = model.renderFlags; for (let sectionPlaneIndex = 0; sectionPlaneIndex < numAllocatedSectionPlanes; sectionPlaneIndex++) { const sectionPlaneUniforms = this._uSectionPlanes[sectionPlaneIndex]; if (sectionPlaneUniforms) { if (sectionPlaneIndex < numSectionPlanes) { const active = renderFlags.sectionPlanesActivePerLayer[baseIndex + sectionPlaneIndex]; gl.uniform1i(sectionPlaneUniforms.active, active ? 1 : 0); if (active) { const sectionPlane = sectionPlanes[sectionPlaneIndex]; if (origin) { const rtcSectionPlanePos = getPlaneRTCPos(sectionPlane.dist, sectionPlane.dir, origin, tempVec3a$i, model.matrix); gl.uniform3fv(sectionPlaneUniforms.pos, rtcSectionPlanePos); } else { gl.uniform3fv(sectionPlaneUniforms.pos, sectionPlane.pos); } gl.uniform3fv(sectionPlaneUniforms.dir, sectionPlane.dir); } } else { gl.uniform1i(sectionPlaneUniforms.active, 0); } } } } if (state.numIndices8Bits > 0) { textureState.bindTriangleIndicesTextures( this._program, this._uTexturePerPolygonIdPortionIds, this._uTexturePerPolygonIdIndices, 8 // 8 bits indices ); gl.drawArrays(gl.TRIANGLES, 0, state.numIndices8Bits); } if (state.numIndices16Bits > 0) { textureState.bindTriangleIndicesTextures( this._program, this._uTexturePerPolygonIdPortionIds, this._uTexturePerPolygonIdIndices, 16 // 16 bits indices ); gl.drawArrays(gl.TRIANGLES, 0, state.numIndices16Bits); } if (state.numIndices32Bits > 0) { textureState.bindTriangleIndicesTextures( this._program, this._uTexturePerPolygonIdPortionIds, this._uTexturePerPolygonIdIndices, 32 // 32 bits indices ); gl.drawArrays(gl.TRIANGLES, 0, state.numIndices32Bits); } frameCtx.drawElements++; } _allocate() { const scene = this._scene; const gl = scene.canvas.gl; this._program = new Program(gl, this._buildShader()); if (this._program.errors) { this.errors = this._program.errors; return; } const program = this._program; this._uRenderPass = program.getLocation("renderPass"); this._uPickInvisible = program.getLocation("pickInvisible"); this._uSceneWorldModelMatrix = program.getLocation("sceneModelMatrix"); this._uViewMatrix = program.getLocation("viewMatrix"); this._uProjMatrix = program.getLocation("projMatrix"); this._uSectionPlanes = []; for (let i = 0, len = scene._sectionPlanesState.getNumAllocatedSectionPlanes(); i < len; i++) { this._uSectionPlanes.push({ active: program.getLocation("sectionPlaneActive" + i), pos: program.getLocation("sectionPlanePos" + i), dir: program.getLocation("sectionPlaneDir" + i) }); } { this._uLogDepthBufFC = program.getLocation("logDepthBufFC"); } this.uTexturePerObjectPositionsDecodeMatrix = "uObjectPerObjectPositionsDecodeMatrix"; this.uTexturePerObjectColorsAndFlags = "uObjectPerObjectColorsAndFlags"; this._uTexturePerVertexIdCoordinates = "uTexturePerVertexIdCoordinates"; this._uTexturePerPolygonIdIndices = "uTexturePerPolygonIdIndices"; this._uTexturePerPolygonIdPortionIds = "uTexturePerPolygonIdPortionIds"; this._uTexturePerObjectMatrix= "uTexturePerObjectMatrix"; this._uCameraEyeRtc = program.getLocation("uCameraEyeRtc"); this._uVectorA = program.getLocation("uVectorAB"); this._uInverseVectorAB = program.getLocation("uInverseVectorAB"); this._uLayerNumber = program.getLocation("uLayerNumber"); this._uCoordinateScaler = program.getLocation("uCoordinateScaler"); } _bindProgram() { this._program.bind(); } _buildShader() { return { vertex: this._buildVertexShader(), fragment: this._buildFragmentShader() }; } _buildVertexShader() { const scene = this._scene; const clipping = scene._sectionPlanesState.getNumAllocatedSectionPlanes() > 0; const src = []; src.push("#version 300 es"); src.push("// DTXTrianglesSnapInitRenderer vertex shader"); src.push("#ifdef GL_FRAGMENT_PRECISION_HIGH"); src.push("precision highp float;"); src.push("precision highp int;"); src.push("precision highp usampler2D;"); src.push("precision highp isampler2D;"); src.push("precision highp sampler2D;"); src.push("#else"); src.push("precision mediump float;"); src.push("precision mediump int;"); src.push("precision mediump usampler2D;"); src.push("precision mediump isampler2D;"); src.push("precision mediump sampler2D;"); src.push("#endif"); src.push("uniform int renderPass;"); if (scene.entityOffsetsEnabled) { src.push("in vec3 offset;"); } src.push("uniform mat4 sceneModelMatrix;"); src.push("uniform mat4 viewMatrix;"); src.push("uniform mat4 projMatrix;"); src.push("uniform highp sampler2D uObjectPerObjectPositionsDecodeMatrix;"); src.push("uniform highp sampler2D uTexturePerObjectMatrix;"); src.push("uniform lowp usampler2D uObjectPerObjectColorsAndFlags;"); src.push("uniform mediump usampler2D uTexturePerVertexIdCoordinates;"); src.push("uniform highp usampler2D uTexturePerPolygonIdIndices;"); src.push("uniform mediump usampler2D uTexturePerPolygonIdPortionIds;"); src.push("uniform vec3 uCameraEyeRtc;"); src.push("uniform vec2 uVectorAB;"); src.push("uniform vec2 uInverseVectorAB;"); src.push("vec3 positions[3];"); { src.push("uniform float logDepthBufFC;"); src.push("out float vFragDepth;"); src.push("out float isPerspective;"); } src.push("bool isPerspectiveMatrix(mat4 m) {"); src.push(" return (m[2][3] == - 1.0);"); src.push("}"); src.push("vec2 remapClipPos(vec2 clipPos) {"); src.push(" float x = (clipPos.x - uVectorAB.x) * uInverseVectorAB.x;"); src.push(" float y = (clipPos.y - uVectorAB.y) * uInverseVectorAB.y;"); src.push(" return vec2(x, y);"); src.push("}"); src.push("flat out vec4 vPickColor;"); src.push("out vec4 vWorldPosition;"); if (clipping) { src.push("flat out uint vFlags2;"); } src.push("out highp vec3 relativeToOriginPosition;"); src.push("void main(void) {"); // constants src.push("int polygonIndex = gl_VertexID / 3;"); // get packed object-id src.push("int h_packed_object_id_index = (polygonIndex >> 3) & 4095;"); src.push("int v_packed_object_id_index = (polygonIndex >> 3) >> 12;"); src.push("int objectIndex = int(texelFetch(uTexturePerPolygonIdPortionIds, ivec2(h_packed_object_id_index, v_packed_object_id_index), 0).r);"); src.push("ivec2 objectIndexCoords = ivec2(objectIndex % 512, objectIndex / 512);"); // get flags & flags2 src.push("uvec4 flags = texelFetch (uObjectPerObjectColorsAndFlags, ivec2(objectIndexCoords.x*8+2, objectIndexCoords.y), 0);"); src.push("uvec4 flags2 = texelFetch (uObjectPerObjectColorsAndFlags, ivec2(objectIndexCoords.x*8+3, objectIndexCoords.y), 0);"); // flags.w = NOT_RENDERED | PICK // renderPass = PICK src.push(`if (int(flags.w) != renderPass) {`); src.push(" gl_Position = vec4(3.0, 3.0, 3.0, 1.0);"); // Cull vertex src.push(" return;"); // Cull vertex src.push("}"); src.push("{"); // get color src.push("uvec4 color = texelFetch (uObjectPerObjectColorsAndFlags, ivec2(objectIndexCoords.x*8+0, objectIndexCoords.y), 0);"); src.push(`if (color.a == 0u) {`); src.push(" gl_Position = vec4(3.0, 3.0, 3.0, 1.0);"); // Cull vertex src.push(" return;"); src.push("};"); // get vertex base src.push("ivec4 packedVertexBase = ivec4(texelFetch (uObjectPerObjectColorsAndFlags, ivec2(objectIndexCoords.x*8+4, objectIndexCoords.y), 0));"); src.push("ivec4 packedIndexBaseOffset = ivec4(texelFetch (uObjectPerObjectColorsAndFlags, ivec2(objectIndexCoords.x*8+5, objectIndexCoords.y), 0));"); src.push("int indexBaseOffset = (packedIndexBaseOffset.r << 24) + (packedIndexBaseOffset.g << 16) + (packedIndexBaseOffset.b << 8) + packedIndexBaseOffset.a;"); src.push("int h_index = (polygonIndex - indexBaseOffset) & 4095;"); src.push("int v_index = (polygonIndex - indexBaseOffset) >> 12;"); src.push("ivec3 vertexIndices = ivec3(texelFetch(uTexturePerPolygonIdIndices, ivec2(h_index, v_index), 0));"); src.push("ivec3 uniqueVertexIndexes = vertexIndices + (packedVertexBase.r << 24) + (packedVertexBase.g << 16) + (packedVertexBase.b << 8) + packedVertexBase.a;"); src.push("ivec3 indexPositionH = uniqueVertexIndexes & 4095;"); src.push("ivec3 indexPositionV = uniqueVertexIndexes >> 12;"); src.push("mat4 objectInstanceMatrix = mat4 (texelFetch (uTexturePerObjectMatrix, ivec2(objectIndexCoords.x*4+0, objectIndexCoords.y), 0), texelFetch (uTexturePerObjectMatrix, ivec2(objectIndexCoords.x*4+1, objectIndexCoords.y), 0), texelFetch (uTexturePerObjectMatrix, ivec2(objectIndexCoords.x*4+2, objectIndexCoords.y), 0), texelFetch (uTexturePerObjectMatrix, ivec2(objectIndexCoords.x*4+3, objectIndexCoords.y), 0));"); src.push("mat4 objectDecodeAndInstanceMatrix = objectInstanceMatrix * mat4 (texelFetch (uObjectPerObjectPositionsDecodeMatrix, ivec2(objectIndexCoords.x*4+0, objectIndexCoords.y), 0), texelFetch (uObjectPerObjectPositionsDecodeMatrix, ivec2(objectIndexCoords.x*4+1, objectIndexCoords.y), 0), texelFetch (uObjectPerObjectPositionsDecodeMatrix, ivec2(objectIndexCoords.x*4+2, objectIndexCoords.y), 0), texelFetch (uObjectPerObjectPositionsDecodeMatrix, ivec2(objectIndexCoords.x*4+3, objectIndexCoords.y), 0));"); src.push("uint solid = texelFetch (uObjectPerObjectColorsAndFlags, ivec2(objectIndexCoords.x*8+7, objectIndexCoords.y), 0).r;"); // get position src.push("positions[0] = vec3(texelFetch(uTexturePerVertexIdCoordinates, ivec2(indexPositionH.r, indexPositionV.r), 0));"); src.push("positions[1] = vec3(texelFetch(uTexturePerVertexIdCoordinates, ivec2(indexPositionH.g, indexPositionV.g), 0));"); src.push("positions[2] = vec3(texelFetch(uTexturePerVertexIdCoordinates, ivec2(indexPositionH.b, indexPositionV.b), 0));"); // get normal src.push("vec3 normal = normalize(cross(positions[2] - positions[0], positions[1] - positions[0]));"); src.push("vec3 position;"); src.push("position = positions[gl_VertexID % 3];"); src.push("vec3 viewNormal = -normalize((transpose(inverse(viewMatrix*objectDecodeAndInstanceMatrix)) * vec4(normal,1)).xyz);"); // when the geometry is not solid, if needed, flip the triangle winding src.push("if (solid != 1u) {"); src.push(" if (isPerspectiveMatrix(projMatrix)) {"); src.push(" vec3 uCameraEyeRtcInQuantizedSpace = (inverse(sceneModelMatrix * objectDecodeAndInstanceMatrix) * vec4(uCameraEyeRtc, 1)).xyz;"); src.push(" if (dot(position.xyz - uCameraEyeRtcInQuantizedSpace, normal) < 0.0) {"); src.push(" position = positions[2 - (gl_VertexID % 3)];"); src.push(" viewNormal = -viewNormal;"); src.push(" }"); src.push(" } else {"); src.push(" if (viewNormal.z < 0.0) {"); src.push(" position = positions[2 - (gl_VertexID % 3)];"); src.push(" viewNormal = -viewNormal;"); src.push(" }"); src.push(" }"); src.push("}"); src.push("vec4 worldPosition = sceneModelMatrix * (objectDecodeAndInstanceMatrix * vec4(position, 1.0)); "); src.push("relativeToOriginPosition = worldPosition.xyz;"); src.push("vec4 viewPosition = viewMatrix * worldPosition; "); src.push("vWorldPosition = worldPosition;"); if (clipping) { src.push("vFlags2 = flags2.r;"); } src.push("vPickColor = vec4(texelFetch (uObjectPerObjectColorsAndFlags, ivec2(objectIndexCoords.x*8+1, objectIndexCoords.y), 0));"); // TODO: Normalized color? See here: //src.push("vPickColor = vec4(texelFetch (uObjectPerObjectColorsAndFlags, ivec2(objectIndexCoords.x*8+1, objectIndexCoords.y), 0)) /255.0;"); src.push("vec4 clipPos = projMatrix * viewPosition;"); src.push("float tmp = clipPos.w;"); src.push("clipPos.xyzw /= tmp;"); src.push("clipPos.xy = remapClipPos(clipPos.xy);"); src.push("clipPos.xyzw *= tmp;"); { src.push("vFragDepth = 1.0 + clipPos.w;"); src.push("isPerspective = float (isPerspectiveMatrix(projMatrix));"); } src.push("gl_Position = clipPos;"); src.push(" }"); src.push("}"); return src; } _buildFragmentShader() { const scene = this._scene; const clipping = scene._sectionPlanesState.getNumAllocatedSectionPlanes() > 0; const src = []; src.push('#version 300 es'); src.push("// DTXTrianglesSnapInitRenderer fragment shader"); src.push("#ifdef GL_FRAGMENT_PRECISION_HIGH"); src.push("precision highp float;"); src.push("precision highp int;"); src.push("#else"); src.push("precision mediump float;"); src.push("precision mediump int;"); src.push("#endif"); { src.push("in float isPerspective;"); src.push("uniform float logDepthBufFC;"); src.push("in float vFragDepth;"); } src.push("uniform int uLayerNumber;"); src.push("uniform vec3 uCoordinateScaler;"); src.push("in vec4 vWorldPosition;"); src.push("flat in vec4 vPickColor;"); if (clipping) { src.push("flat in uint vFlags2;"); for (let i = 0, len = scene._sectionPlanesState.getNumAllocatedSectionPlanes(); i < len; i++) { src.push("uniform bool sectionPlaneActive" + i + ";"); src.push("uniform vec3 sectionPlanePos" + i + ";"); src.push("uniform vec3 sectionPlaneDir" + i + ";"); } } src.push("in highp vec3 relativeToOriginPosition;"); src.push("layout(location = 0) out highp ivec4 outCoords;"); src.push("layout(location = 1) out highp ivec4 outNormal;"); src.push("layout(location = 2) out lowp uvec4 outPickColor;"); src.push("void main(void) {"); if (clipping) { src.push(" bool clippable = vFlags2 > 0u;"); src.push(" if (clippable) {"); src.push(" float dist = 0.0;"); for (var i = 0; i < scene._sectionPlanesState.getNumAllocatedSectionPlanes(); i++) { src.push(" if (sectionPlaneActive" + i + ") {"); src.push(" dist += clamp(dot(-sectionPlaneDir" + i + ".xyz, vWorldPosition.xyz - sectionPlanePos" + i + ".xyz), 0.0, 1000.0);"); src.push(" }"); } src.push(" if (dist > 0.0) { discard; }"); src.push(" }"); } { src.push(" float dx = dFdx(vFragDepth);"); src.push(" float dy = dFdy(vFragDepth);"); src.push(" float diff = sqrt(dx*dx+dy*dy);"); src.push(" gl_FragDepth = isPerspective == 0.0 ? gl_FragCoord.z : log2( vFragDepth + diff ) * logDepthBufFC * 0.5;"); } src.push("outCoords = ivec4(relativeToOriginPosition.xyz * uCoordinateScaler.xyz, - uLayerNumber);"); src.push("vec3 xTangent = dFdx( vWorldPosition.xyz );"); src.push("vec3 yTangent = dFdy( vWorldPosition.xyz );"); src.push("vec3 worldNormal = normalize( cross( xTangent, yTangent ) );"); src.push(`outNormal = ivec4(worldNormal * float(${math.MAX_INT}), 1.0);`); src.push("outPickColor = uvec4(vPickColor);"); src.push("}"); return src; } webglContextRestored() { this._program = null; } destroy() { if (this._program) { this._program.destroy(); } this._program = null; } } const tempVec3a$h = math.vec3(); const tempVec3b$d = math.vec3(); const tempVec3c$b = math.vec3(); math.vec3(); const tempMat4a$4 = math.mat4(); /** * @private */ class DTXTrianglesOcclusionRenderer { constructor(scene) { this._scene = scene; this._hash = this._getHash(); this._allocate(); } getValid() { return this._hash === this._getHash(); }; _getHash() { return this._scene._sectionPlanesState.getHash(); } drawLayer(frameCtx, dataTextureLayer, renderPass) { const model = dataTextureLayer.model; const scene = model.scene; const camera = scene.camera; const gl = scene.canvas.gl; const state = dataTextureLayer._state; const textureState = state.textureState; const origin = dataTextureLayer._state.origin; const {position, rotationMatrix} = model; const viewMatrix = frameCtx.pickViewMatrix || camera.viewMatrix; if (!this._program) { this._allocate(dataTextureLayer); if (this.errors) { return; } } if (frameCtx.lastProgramId !== this._program.id) { frameCtx.lastProgramId = this._program.id; this._bindProgram(); } textureState.bindCommonTextures( this._program, this.uTexturePerObjectPositionsDecodeMatrix, this._uTexturePerVertexIdCoordinates, this.uTexturePerObjectColorsAndFlags, this._uTexturePerObjectMatrix ); let rtcViewMatrix; let rtcCameraEye; if (origin || position[0] !== 0 || position[1] !== 0 || position[2] !== 0) { const rtcOrigin = tempVec3a$h; if (origin) { const rotatedOrigin = tempVec3b$d; math.transformPoint3(rotationMatrix, origin, rotatedOrigin); rtcOrigin[0] = rotatedOrigin[0]; rtcOrigin[1] = rotatedOrigin[1]; rtcOrigin[2] = rotatedOrigin[2]; } else { rtcOrigin[0] = 0; rtcOrigin[1] = 0; rtcOrigin[2] = 0; } rtcOrigin[0] += position[0]; rtcOrigin[1] += position[1]; rtcOrigin[2] += position[2]; rtcViewMatrix = createRTCViewMat(viewMatrix, rtcOrigin, tempMat4a$4); rtcCameraEye = tempVec3c$b; rtcCameraEye[0] = camera.eye[0] - rtcOrigin[0]; rtcCameraEye[1] = camera.eye[1] - rtcOrigin[1]; rtcCameraEye[2] = camera.eye[2] - rtcOrigin[2]; } else { rtcViewMatrix = viewMatrix; rtcCameraEye = camera.eye; } gl.uniform3fv(this._uCameraEyeRtc, rtcCameraEye); gl.uniform1i(this._uRenderPass, renderPass); gl.uniformMatrix4fv(this._uWorldMatrix, false, rotationMatrix); gl.uniformMatrix4fv(this._uViewMatrix, false, rtcViewMatrix); gl.uniformMatrix4fv(this._uProjMatrix, false, camera.projMatrix); const numAllocatedSectionPlanes = scene._sectionPlanesState.getNumAllocatedSectionPlanes(); const numSectionPlanes = scene._sectionPlanesState.sectionPlanes.length; if (numAllocatedSectionPlanes > 0) { const sectionPlanes = scene._sectionPlanesState.sectionPlanes; const baseIndex = dataTextureLayer.layerIndex * numSectionPlanes; const renderFlags = model.renderFlags; for (let sectionPlaneIndex = 0; sectionPlaneIndex < numAllocatedSectionPlanes; sectionPlaneIndex++) { const sectionPlaneUniforms = this._uSectionPlanes[sectionPlaneIndex]; if (sectionPlaneUniforms) { if (sectionPlaneIndex < numSectionPlanes) { const active = renderFlags.sectionPlanesActivePerLayer[baseIndex + sectionPlaneIndex]; gl.uniform1i(sectionPlaneUniforms.active, active ? 1 : 0); if (active) { const sectionPlane = sectionPlanes[sectionPlaneIndex]; if (origin) { const rtcSectionPlanePos = getPlaneRTCPos(sectionPlane.dist, sectionPlane.dir, origin, tempVec3a$h, model.matrix); gl.uniform3fv(sectionPlaneUniforms.pos, rtcSectionPlanePos); } else { gl.uniform3fv(sectionPlaneUniforms.pos, sectionPlane.pos); } gl.uniform3fv(sectionPlaneUniforms.dir, sectionPlane.dir); } } else { gl.uniform1i(sectionPlaneUniforms.active, 0); } } } } if (state.numIndices8Bits > 0) { textureState.bindTriangleIndicesTextures( this._program, this._uTexturePerPolygonIdPortionIds, this._uTexturePerPolygonIdIndices, 8 // 8 bits indices ); gl.drawArrays(gl.TRIANGLES, 0, state.numIndices8Bits); } if (state.numIndices16Bits > 0) { textureState.bindTriangleIndicesTextures( this._program, this._uTexturePerPolygonIdPortionIds, this._uTexturePerPolygonIdIndices, 16 // 16 bits indices ); gl.drawArrays(gl.TRIANGLES, 0, state.numIndices16Bits); } if (state.numIndices32Bits > 0) { textureState.bindTriangleIndicesTextures( this._program, this._uTexturePerPolygonIdPortionIds, this._uTexturePerPolygonIdIndices, 32 // 32 bits indices ); gl.drawArrays(gl.TRIANGLES, 0, state.numIndices32Bits); } frameCtx.drawElements++; } _allocate() { const scene = this._scene; const gl = scene.canvas.gl; this._program = new Program(gl, this._buildShader()); if (this._program.errors) { this.errors = this._program.errors; return; } const program = this._program; this._uRenderPass = program.getLocation("renderPass"); this._uPickInvisible = program.getLocation("pickInvisible"); this._uWorldMatrix = program.getLocation("sceneModelMatrix"); this._uViewMatrix = program.getLocation("viewMatrix"); this._uProjMatrix = program.getLocation("projMatrix"); this._uSectionPlanes = []; for (let i = 0, len = scene._sectionPlanesState.getNumAllocatedSectionPlanes(); i < len; i++) { this._uSectionPlanes.push({ active: program.getLocation("sectionPlaneActive" + i), pos: program.getLocation("sectionPlanePos" + i), dir: program.getLocation("sectionPlaneDir" + i) }); } this._uPickZNear = program.getLocation("pickZNear"); this._uPickZFar = program.getLocation("pickZFar"); if (scene.logarithmicDepthBufferEnabled) { this._uLogDepthBufFC = program.getLocation("logDepthBufFC"); } this.uTexturePerObjectPositionsDecodeMatrix = "uObjectPerObjectPositionsDecodeMatrix"; this.uTexturePerObjectColorsAndFlags = "uObjectPerObjectColorsAndFlags"; this._uTexturePerVertexIdCoordinates = "uTexturePerVertexIdCoordinates"; this._uTexturePerPolygonIdNormals = "uTexturePerPolygonIdNormals"; this._uTexturePerPolygonIdIndices = "uTexturePerPolygonIdIndices"; this._uTexturePerPolygonIdPortionIds = "uTexturePerPolygonIdPortionIds"; this._uTexturePerObjectMatrix= "uTexturePerObjectMatrix"; this._uCameraEyeRtc = program.getLocation("uCameraEyeRtc"); } _bindProgram() { const scene = this._scene; scene.canvas.gl; scene.camera.project; this._program.bind(); } _buildShader() { return { vertex: this._buildVertexShader(), fragment: this._buildFragmentShader() }; } _buildVertexShader() { const scene = this._scene; const clipping = scene._sectionPlanesState.getNumAllocatedSectionPlanes() > 0; const src = []; src.push("#version 300 es"); src.push("// TrianglesDataTextureOcclusionRenderer vertex shader"); src.push("#ifdef GL_FRAGMENT_PRECISION_HIGH"); src.push("precision highp float;"); src.push("precision highp int;"); src.push("precision highp usampler2D;"); src.push("precision highp isampler2D;"); src.push("precision highp sampler2D;"); src.push("#else"); src.push("precision mediump float;"); src.push("precision mediump int;"); src.push("precision mediump usampler2D;"); src.push("precision mediump isampler2D;"); src.push("precision mediump sampler2D;"); src.push("#endif"); src.push("uniform int renderPass;"); if (scene.entityOffsetsEnabled) { src.push("in vec3 offset;"); } src.push("uniform mat4 sceneModelMatrix;"); src.push("uniform mat4 viewMatrix;"); src.push("uniform mat4 projMatrix;"); src.push("uniform highp sampler2D uObjectPerObjectPositionsDecodeMatrix;"); src.push("uniform highp sampler2D uTexturePerObjectMatrix;"); src.push("uniform lowp usampler2D uObjectPerObjectColorsAndFlags;"); src.push("uniform mediump usampler2D uTexturePerVertexIdCoordinates;"); src.push("uniform highp usampler2D uTexturePerPolygonIdIndices;"); src.push("uniform mediump usampler2D uTexturePerPolygonIdPortionIds;"); src.push("uniform vec3 uCameraEyeRtc;"); src.push("vec3 positions[3];"); src.push("bool isPerspectiveMatrix(mat4 m) {"); src.push(" return (m[2][3] == - 1.0);"); src.push("}"); if (clipping) { src.push("out vec4 vWorldPosition;"); src.push("flat out uint vFlags2;"); } src.push("void main(void) {"); // constants src.push("int polygonIndex = gl_VertexID / 3;"); // get packed object-id src.push("int h_packed_object_id_index = (polygonIndex >> 3) & 4095;"); src.push("int v_packed_object_id_index = (polygonIndex >> 3) >> 12;"); src.push("int objectIndex = int(texelFetch(uTexturePerPolygonIdPortionIds, ivec2(h_packed_object_id_index, v_packed_object_id_index), 0).r);"); src.push("ivec2 objectIndexCoords = ivec2(objectIndex % 512, objectIndex / 512);"); // get flags & flags2 src.push("uvec4 flags = texelFetch (uObjectPerObjectColorsAndFlags, ivec2(objectIndexCoords.x*8+2, objectIndexCoords.y), 0);"); src.push("uvec4 flags2 = texelFetch (uObjectPerObjectColorsAndFlags, ivec2(objectIndexCoords.x*8+3, objectIndexCoords.y), 0);"); // flags.x = NOT_RENDERED | COLOR_OPAQUE | COLOR_TRANSPARENT // renderPass = COLOR_OPAQUE // Only opaque objects can be occluders src.push(`if (int(flags.x) != renderPass) {`); src.push(" gl_Position = vec4(0.0, 0.0, 0.0, 0.0);"); // Cull vertex src.push(" } else {"); // get vertex base src.push("ivec4 packedVertexBase = ivec4(texelFetch (uObjectPerObjectColorsAndFlags, ivec2(objectIndexCoords.x*8+4, objectIndexCoords.y), 0));"); src.push("ivec4 packedIndexBaseOffset = ivec4(texelFetch (uObjectPerObjectColorsAndFlags, ivec2(objectIndexCoords.x*8+5, objectIndexCoords.y), 0));"); src.push("int indexBaseOffset = (packedIndexBaseOffset.r << 24) + (packedIndexBaseOffset.g << 16) + (packedIndexBaseOffset.b << 8) + packedIndexBaseOffset.a;"); src.push("int h_index = (polygonIndex - indexBaseOffset) & 4095;"); src.push("int v_index = (polygonIndex - indexBaseOffset) >> 12;"); src.push("ivec3 vertexIndices = ivec3(texelFetch(uTexturePerPolygonIdIndices, ivec2(h_index, v_index), 0));"); src.push("ivec3 uniqueVertexIndexes = vertexIndices + (packedVertexBase.r << 24) + (packedVertexBase.g << 16) + (packedVertexBase.b << 8) + packedVertexBase.a;"); src.push("ivec3 indexPositionH = uniqueVertexIndexes & 4095;"); src.push("ivec3 indexPositionV = uniqueVertexIndexes >> 12;"); src.push("mat4 objectInstanceMatrix = mat4 (texelFetch (uTexturePerObjectMatrix, ivec2(objectIndexCoords.x*4+0, objectIndexCoords.y), 0), texelFetch (uTexturePerObjectMatrix, ivec2(objectIndexCoords.x*4+1, objectIndexCoords.y), 0), texelFetch (uTexturePerObjectMatrix, ivec2(objectIndexCoords.x*4+2, objectIndexCoords.y), 0), texelFetch (uTexturePerObjectMatrix, ivec2(objectIndexCoords.x*4+3, objectIndexCoords.y), 0));"); src.push("mat4 objectDecodeAndInstanceMatrix = objectInstanceMatrix * mat4 (texelFetch (uObjectPerObjectPositionsDecodeMatrix, ivec2(objectIndexCoords.x*4+0, objectIndexCoords.y), 0), texelFetch (uObjectPerObjectPositionsDecodeMatrix, ivec2(objectIndexCoords.x*4+1, objectIndexCoords.y), 0), texelFetch (uObjectPerObjectPositionsDecodeMatrix, ivec2(objectIndexCoords.x*4+2, objectIndexCoords.y), 0), texelFetch (uObjectPerObjectPositionsDecodeMatrix, ivec2(objectIndexCoords.x*4+3, objectIndexCoords.y), 0));"); src.push("uint solid = texelFetch (uObjectPerObjectColorsAndFlags, ivec2(objectIndexCoords.x*8+7, objectIndexCoords.y), 0).r;"); // get position src.push("positions[0] = vec3(texelFetch(uTexturePerVertexIdCoordinates, ivec2(indexPositionH.r, indexPositionV.r), 0));"); src.push("positions[1] = vec3(texelFetch(uTexturePerVertexIdCoordinates, ivec2(indexPositionH.g, indexPositionV.g), 0));"); src.push("positions[2] = vec3(texelFetch(uTexturePerVertexIdCoordinates, ivec2(indexPositionH.b, indexPositionV.b), 0));"); // get color src.push("uvec4 color = texelFetch (uObjectPerObjectColorsAndFlags, ivec2(objectIndexCoords.x*8+0, objectIndexCoords.y), 0);"); src.push(`if (color.a == 0u) {`); src.push(" gl_Position = vec4(3.0, 3.0, 3.0, 1.0);"); // Cull vertex src.push(" return;"); src.push("};"); src.push("vec3 normal = normalize(cross(positions[2] - positions[0], positions[1] - positions[0]));"); src.push("vec3 position;"); src.push("position = positions[gl_VertexID % 3];"); // when the geometry is not solid, if needed, flip the triangle winding src.push("if (solid != 1u) {"); src.push(" if (isPerspectiveMatrix(projMatrix)) {"); src.push(" vec3 uCameraEyeRtcInQuantizedSpace = (inverse(sceneModelMatrix * objectDecodeAndInstanceMatrix) * vec4(uCameraEyeRtc, 1)).xyz;"); src.push(" if (dot(position.xyz - uCameraEyeRtcInQuantizedSpace, normal) < 0.0) {"); src.push(" position = positions[2 - (gl_VertexID % 3)];"); src.push(" }"); src.push(" } else {"); src.push(" vec3 viewNormal = -normalize((transpose(inverse(viewMatrix*objectDecodeAndInstanceMatrix)) * vec4(normal,1)).xyz);"); src.push(" if (viewNormal.z < 0.0) {"); src.push(" position = positions[2 - (gl_VertexID % 3)];"); src.push(" }"); src.push(" }"); src.push("}"); src.push("vec4 worldPosition = sceneModelMatrix * (objectDecodeAndInstanceMatrix * vec4(position, 1.0)); "); src.push("vec4 viewPosition = viewMatrix * worldPosition; "); src.push("vec4 clipPos = projMatrix * viewPosition;"); if (clipping) { src.push("vWorldPosition = worldPosition;"); src.push("vFlags2 = flags2.r;"); } src.push("gl_Position = clipPos;"); src.push(" }"); src.push("}"); return src; } _buildFragmentShader() { const scene = this._scene; const sectionPlanesState = scene._sectionPlanesState; const clipping = sectionPlanesState.getNumAllocatedSectionPlanes() > 0; const src = []; src.push('#version 300 es'); src.push("// TrianglesDataTextureColorRenderer fragment shader"); src.push("#ifdef GL_FRAGMENT_PRECISION_HIGH"); src.push("precision highp float;"); src.push("precision highp int;"); src.push("#else"); src.push("precision mediump float;"); src.push("precision mediump int;"); src.push("#endif"); if (clipping) { src.push("in vec4 vWorldPosition;"); src.push("flat in uint vFlags2;"); for (let i = 0; i < sectionPlanesState.getNumAllocatedSectionPlanes(); i++) { src.push("uniform bool sectionPlaneActive" + i + ";"); src.push("uniform vec3 sectionPlanePos" + i + ";"); src.push("uniform vec3 sectionPlaneDir" + i + ";"); } } src.push("out vec4 outColor;"); src.push("void main(void) {"); if (clipping) { src.push(" bool clippable = (float(vFlags2) > 0.0);"); src.push(" if (clippable) {"); src.push(" float dist = 0.0;"); for (let i = 0; i < sectionPlanesState.getNumAllocatedSectionPlanes(); i++) { src.push(" if (sectionPlaneActive" + i + ") {"); src.push(" dist += clamp(dot(-sectionPlaneDir" + i + ".xyz, vWorldPosition.xyz - sectionPlanePos" + i + ".xyz), 0.0, 1000.0);"); src.push(" }"); } src.push(" if (dist > 0.0) { discard; }"); src.push(" }"); } src.push(" outColor = vec4(0.0, 0.0, 1.0, 1.0); "); // Occluders are blue src.push("}"); return src; } webglContextRestored() { this._program = null; } destroy() { if (this._program) { this._program.destroy(); } this._program = null; } } const tempVec3a$g = math.vec3(); const tempVec3b$c = math.vec3(); const tempVec3c$a = math.vec3(); math.vec3(); const tempMat4a$3 = math.mat4(); /** * @private */ class DTXTrianglesDepthRenderer { constructor(scene) { this._scene = scene; this._allocate(); this._hash = this._getHash(); } getValid() { return this._hash === this._getHash(); }; _getHash() { return this._scene._sectionPlanesState.getHash(); } drawLayer(frameCtx, dataTextureLayer, renderPass) { const scene = this._scene; const camera = scene.camera; const model = dataTextureLayer.model; const gl = scene.canvas.gl; const state = dataTextureLayer._state; const textureState = state.textureState; const origin = dataTextureLayer._state.origin; const {position, rotationMatrix} = model; if (!this._program) { this._allocate(); if (this.errors) { return; } } if (frameCtx.lastProgramId !== this._program.id) { frameCtx.lastProgramId = this._program.id; this._bindProgram(frameCtx, state); } textureState.bindCommonTextures( this._program, this.uTexturePerObjectPositionsDecodeMatrix, this._uTexturePerVertexIdCoordinates, this.uTexturePerObjectColorsAndFlags, this._uTexturePerObjectMatrix ); let rtcViewMatrix; let rtcCameraEye; const gotOrigin = (origin[0] !== 0 || origin[1] !== 0 || origin[2] !== 0); const gotPosition = (position[0] !== 0 || position[1] !== 0 || position[2] !== 0); if (gotOrigin || gotPosition) { const rtcOrigin = tempVec3a$g; if (gotOrigin) { const rotatedOrigin = math.transformPoint3(rotationMatrix, origin, tempVec3b$c); rtcOrigin[0] = rotatedOrigin[0]; rtcOrigin[1] = rotatedOrigin[1]; rtcOrigin[2] = rotatedOrigin[2]; } else { rtcOrigin[0] = 0; rtcOrigin[1] = 0; rtcOrigin[2] = 0; } rtcOrigin[0] += position[0]; rtcOrigin[1] += position[1]; rtcOrigin[2] += position[2]; rtcViewMatrix = createRTCViewMat(camera.viewMatrix, rtcOrigin, tempMat4a$3); rtcCameraEye = tempVec3c$a; rtcCameraEye[0] = camera.eye[0] - rtcOrigin[0]; rtcCameraEye[1] = camera.eye[1] - rtcOrigin[1]; rtcCameraEye[2] = camera.eye[2] - rtcOrigin[2]; } else { rtcViewMatrix = camera.viewMatrix; rtcCameraEye = camera.eye; } gl.uniformMatrix4fv(this._uSceneModelMatrix, false, rotationMatrix); gl.uniformMatrix4fv(this._uViewMatrix, false, rtcViewMatrix); gl.uniformMatrix4fv(this._uProjMatrix, false, camera.projMatrix); gl.uniform3fv(this._uCameraEyeRtc, rtcCameraEye); gl.uniform1i(this._uRenderPass, renderPass); if (scene.logarithmicDepthBufferEnabled) { const logDepthBufFC = 2.0 / (Math.log(frameCtx.pickZFar + 1.0) / Math.LN2); gl.uniform1f(this._uLogDepthBufFC, logDepthBufFC); } const numAllocatedSectionPlanes = scene._sectionPlanesState.getNumAllocatedSectionPlanes(); const numSectionPlanes = scene._sectionPlanesState.sectionPlanes.length; if (numAllocatedSectionPlanes > 0) { const sectionPlanes = scene._sectionPlanesState.sectionPlanes; const baseIndex = dataTextureLayer.layerIndex * numSectionPlanes; const renderFlags = model.renderFlags; for (let sectionPlaneIndex = 0; sectionPlaneIndex < numAllocatedSectionPlanes; sectionPlaneIndex++) { const sectionPlaneUniforms = this._uSectionPlanes[sectionPlaneIndex]; if (sectionPlaneUniforms) { if (sectionPlaneIndex < numSectionPlanes) { const active = renderFlags.sectionPlanesActivePerLayer[baseIndex + sectionPlaneIndex]; gl.uniform1i(sectionPlaneUniforms.active, active ? 1 : 0); if (active) { const sectionPlane = sectionPlanes[sectionPlaneIndex]; if (origin) { const rtcSectionPlanePos = getPlaneRTCPos(sectionPlane.dist, sectionPlane.dir, origin, tempVec3a$g, model.matrix); gl.uniform3fv(sectionPlaneUniforms.pos, rtcSectionPlanePos); } else { gl.uniform3fv(sectionPlaneUniforms.pos, sectionPlane.pos); } gl.uniform3fv(sectionPlaneUniforms.dir, sectionPlane.dir); } } else { gl.uniform1i(sectionPlaneUniforms.active, 0); } } } } if (state.numIndices8Bits > 0) { textureState.bindTriangleIndicesTextures( this._program, this._uTexturePerPolygonIdPortionIds, this._uTexturePerPolygonIdIndices, 8 // 8 bits indices ); gl.drawArrays(gl.TRIANGLES, 0, state.numIndices8Bits); } if (state.numIndices16Bits > 0) { textureState.bindTriangleIndicesTextures( this._program, this._uTexturePerPolygonIdPortionIds, this._uTexturePerPolygonIdIndices, 16 // 16 bits indices ); gl.drawArrays(gl.TRIANGLES, 0, state.numIndices16Bits); } if (state.numIndices32Bits > 0) { textureState.bindTriangleIndicesTextures( this._program, this._uTexturePerPolygonIdPortionIds, this._uTexturePerPolygonIdIndices, 32 // 32 bits indices ); gl.drawArrays(gl.TRIANGLES, 0, state.numIndices32Bits); } frameCtx.drawElements++; } _allocate() { const scene = this._scene; const gl = scene.canvas.gl; this._program = new Program(gl, this._buildShader()); if (this._program.errors) { this.errors = this._program.errors; return; } const program = this._program; this._uRenderPass = program.getLocation("renderPass"); this._uPositionsDecodeMatrix = program.getLocation("objectDecodeAndInstanceMatrix"); this._uSceneModelMatrix = program.getLocation("sceneModelMatrix"); this._uViewMatrix = program.getLocation("viewMatrix"); this._uProjMatrix = program.getLocation("projMatrix"); this._uSectionPlanes = []; for (let i = 0, len = scene._sectionPlanesState.getNumAllocatedSectionPlanes(); i < len; i++) { this._uSectionPlanes.push({ active: program.getLocation("sectionPlaneActive" + i), pos: program.getLocation("sectionPlanePos" + i), dir: program.getLocation("sectionPlaneDir" + i) }); } if (scene.logarithmicDepthBufferEnabled) { this._uLogDepthBufFC = program.getLocation("logDepthBufFC"); } this.uTexturePerObjectPositionsDecodeMatrix = "uObjectPerObjectPositionsDecodeMatrix"; this.uTexturePerObjectColorsAndFlags = "uObjectPerObjectColorsAndFlags"; this._uTexturePerVertexIdCoordinates = "uTexturePerVertexIdCoordinates"; this._uTexturePerPolygonIdNormals = "uTexturePerPolygonIdNormals"; this._uTexturePerPolygonIdIndices = "uTexturePerPolygonIdIndices"; this._uTexturePerPolygonIdPortionIds = "uTexturePerPolygonIdPortionIds"; this._uTexturePerObjectMatrix= "uTexturePerObjectMatrix"; this._uCameraEyeRtc = program.getLocation("uCameraEyeRtc"); } _bindProgram(frameCtx) { const scene = this._scene; const gl = scene.canvas.gl; const program = this._program; const project = scene.camera.project; program.bind(); gl.uniformMatrix4fv(this._uProjMatrix, false, project.matrix); if (scene.logarithmicDepthBufferEnabled) { const logDepthBufFC = 2.0 / (Math.log(project.far + 1.0) / Math.LN2); gl.uniform1f(this._uLogDepthBufFC, logDepthBufFC); } } _buildShader() { return { vertex: this._buildVertexShader(), fragment: this._buildFragmentShader() }; } _buildVertexShader() { const scene = this._scene; const clipping = scene._sectionPlanesState.getNumAllocatedSectionPlanes() > 0; const src = []; src.push("#version 300 es"); src.push("// Triangles dataTexture draw vertex shader"); src.push("#ifdef GL_FRAGMENT_PRECISION_HIGH"); src.push("precision highp float;"); src.push("precision highp int;"); src.push("precision highp usampler2D;"); src.push("precision highp isampler2D;"); src.push("precision highp sampler2D;"); src.push("#else"); src.push("precision mediump float;"); src.push("precision mediump int;"); src.push("precision mediump usampler2D;"); src.push("precision mediump isampler2D;"); src.push("precision mediump sampler2D;"); src.push("#endif"); src.push("uniform int renderPass;"); if (scene.entityOffsetsEnabled) { src.push("in vec3 offset;"); } src.push("uniform mat4 sceneModelMatrix;"); src.push("uniform mat4 viewMatrix;"); src.push("uniform mat4 projMatrix;"); src.push("uniform highp sampler2D uObjectPerObjectPositionsDecodeMatrix;"); src.push("uniform highp sampler2D uTexturePerObjectMatrix;"); src.push("uniform lowp usampler2D uObjectPerObjectColorsAndFlags;"); src.push("uniform mediump usampler2D uTexturePerVertexIdCoordinates;"); src.push("uniform highp usampler2D uTexturePerPolygonIdIndices;"); src.push("uniform mediump usampler2D uTexturePerPolygonIdPortionIds;"); src.push("uniform vec3 uCameraEyeRtc;"); src.push("vec3 positions[3];"); if (scene.logarithmicDepthBufferEnabled) { src.push("uniform float logDepthBufFC;"); src.push("out float vFragDepth;"); src.push("out float isPerspective;"); } src.push("bool isPerspectiveMatrix(mat4 m) {"); src.push(" return (m[2][3] == - 1.0);"); src.push("}"); src.push("out highp vec2 vHighPrecisionZW;"); if (clipping) { src.push("out vec4 vWorldPosition;"); src.push("flat out uint vFlags2;"); } src.push("void main(void) {"); // constants src.push("int polygonIndex = gl_VertexID / 3;"); // get packed object-id src.push("int h_packed_object_id_index = (polygonIndex >> 3) & 4095;"); src.push("int v_packed_object_id_index = (polygonIndex >> 3) >> 12;"); src.push("int objectIndex = int(texelFetch(uTexturePerPolygonIdPortionIds, ivec2(h_packed_object_id_index, v_packed_object_id_index), 0).r);"); src.push("ivec2 objectIndexCoords = ivec2(objectIndex % 512, objectIndex / 512);"); // get flags & flags2 src.push("uvec4 flags = texelFetch (uObjectPerObjectColorsAndFlags, ivec2(objectIndexCoords.x*8+2, objectIndexCoords.y), 0);"); src.push("uvec4 flags2 = texelFetch (uObjectPerObjectColorsAndFlags, ivec2(objectIndexCoords.x*8+3, objectIndexCoords.y), 0);"); // flags.x = NOT_RENDERED | COLOR_OPAQUE | COLOR_TRANSPARENT // renderPass = COLOR_OPAQUE src.push(`if (int(flags.x) != renderPass) {`); src.push(" gl_Position = vec4(3.0, 3.0, 3.0, 1.0);"); // Cull vertex src.push(" return;"); // Cull vertex src.push("} else {"); // get vertex base src.push("ivec4 packedVertexBase = ivec4(texelFetch (uObjectPerObjectColorsAndFlags, ivec2(objectIndexCoords.x*8+4, objectIndexCoords.y), 0));"); src.push("ivec4 packedIndexBaseOffset = ivec4(texelFetch (uObjectPerObjectColorsAndFlags, ivec2(objectIndexCoords.x*8+5, objectIndexCoords.y), 0));"); src.push("int indexBaseOffset = (packedIndexBaseOffset.r << 24) + (packedIndexBaseOffset.g << 16) + (packedIndexBaseOffset.b << 8) + packedIndexBaseOffset.a;"); src.push("int h_index = (polygonIndex - indexBaseOffset) & 4095;"); src.push("int v_index = (polygonIndex - indexBaseOffset) >> 12;"); src.push("ivec3 vertexIndices = ivec3(texelFetch(uTexturePerPolygonIdIndices, ivec2(h_index, v_index), 0));"); src.push("ivec3 uniqueVertexIndexes = vertexIndices + (packedVertexBase.r << 24) + (packedVertexBase.g << 16) + (packedVertexBase.b << 8) + packedVertexBase.a;"); src.push("ivec3 indexPositionH = uniqueVertexIndexes & 4095;"); src.push("ivec3 indexPositionV = uniqueVertexIndexes >> 12;"); src.push("mat4 objectInstanceMatrix = mat4 (texelFetch (uTexturePerObjectMatrix, ivec2(objectIndexCoords.x*4+0, objectIndexCoords.y), 0), texelFetch (uTexturePerObjectMatrix, ivec2(objectIndexCoords.x*4+1, objectIndexCoords.y), 0), texelFetch (uTexturePerObjectMatrix, ivec2(objectIndexCoords.x*4+2, objectIndexCoords.y), 0), texelFetch (uTexturePerObjectMatrix, ivec2(objectIndexCoords.x*4+3, objectIndexCoords.y), 0));"); src.push("mat4 objectDecodeAndInstanceMatrix = objectInstanceMatrix * mat4 (texelFetch (uObjectPerObjectPositionsDecodeMatrix, ivec2(objectIndexCoords.x*4+0, objectIndexCoords.y), 0), texelFetch (uObjectPerObjectPositionsDecodeMatrix, ivec2(objectIndexCoords.x*4+1, objectIndexCoords.y), 0), texelFetch (uObjectPerObjectPositionsDecodeMatrix, ivec2(objectIndexCoords.x*4+2, objectIndexCoords.y), 0), texelFetch (uObjectPerObjectPositionsDecodeMatrix, ivec2(objectIndexCoords.x*4+3, objectIndexCoords.y), 0));"); src.push("uint solid = texelFetch (uObjectPerObjectColorsAndFlags, ivec2(objectIndexCoords.x*8+7, objectIndexCoords.y), 0).r;"); // get position src.push("positions[0] = vec3(texelFetch(uTexturePerVertexIdCoordinates, ivec2(indexPositionH.r, indexPositionV.r), 0));"); src.push("positions[1] = vec3(texelFetch(uTexturePerVertexIdCoordinates, ivec2(indexPositionH.g, indexPositionV.g), 0));"); src.push("positions[2] = vec3(texelFetch(uTexturePerVertexIdCoordinates, ivec2(indexPositionH.b, indexPositionV.b), 0));"); // get color src.push("uvec4 color = texelFetch (uObjectPerObjectColorsAndFlags, ivec2(objectIndexCoords.x*8+0, objectIndexCoords.y), 0);"); src.push(`if (color.a == 0u) {`); src.push(" gl_Position = vec4(3.0, 3.0, 3.0, 1.0);"); // Cull vertex src.push(" return;"); src.push("};"); // get normal src.push("vec3 normal = normalize(cross(positions[2] - positions[0], positions[1] - positions[0]));"); src.push("vec3 position;"); src.push("position = positions[gl_VertexID % 3];"); src.push("vec3 viewNormal = -normalize((transpose(inverse(viewMatrix*objectDecodeAndInstanceMatrix)) * vec4(normal,1)).xyz);"); // when the geometry is not solid, if needed, flip the triangle winding src.push("if (solid != 1u) {"); src.push("if (isPerspectiveMatrix(projMatrix)) {"); src.push("vec3 uCameraEyeRtcInQuantizedSpace = (inverse(sceneModelMatrix * objectDecodeAndInstanceMatrix) * vec4(uCameraEyeRtc, 1)).xyz;"); src.push("if (dot(position.xyz - uCameraEyeRtcInQuantizedSpace, normal) < 0.0) {"); src.push("position = positions[2 - (gl_VertexID % 3)];"); src.push("viewNormal = -viewNormal;"); src.push("}"); src.push("} else {"); src.push("if (viewNormal.z < 0.0) {"); src.push("position = positions[2 - (gl_VertexID % 3)];"); src.push("viewNormal = -viewNormal;"); src.push("}"); src.push("}"); src.push("}"); src.push("vec4 worldPosition = sceneModelMatrix * (objectDecodeAndInstanceMatrix * vec4(position, 1.0)); "); src.push("vec4 viewPosition = viewMatrix * worldPosition; "); src.push("vec4 clipPos = projMatrix * viewPosition;"); if (scene.logarithmicDepthBufferEnabled) { src.push("vFragDepth = 1.0 + clipPos.w;"); src.push("isPerspective = float (isPerspectiveMatrix(projMatrix));"); } if (clipping) { src.push("vWorldPosition = worldPosition;"); src.push("vFlags2 = flags2.r;"); } src.push("gl_Position = clipPos;"); src.push("vHighPrecisionZW = gl_Position.zw;"); src.push("}"); src.push("}"); return src; } _buildFragmentShader() { const scene = this._scene; const clipping = scene._sectionPlanesState.getNumAllocatedSectionPlanes() > 0; const src = []; src.push('#version 300 es'); src.push("// Triangles dataTexture draw fragment shader"); src.push("#ifdef GL_FRAGMENT_PRECISION_HIGH"); src.push("precision highp float;"); src.push("precision highp int;"); src.push("#else"); src.push("precision mediump float;"); src.push("precision mediump int;"); src.push("#endif"); src.push("in highp vec2 vHighPrecisionZW;"); src.push("out vec4 outColor;"); if (scene.logarithmicDepthBufferEnabled) { src.push("in float isPerspective;"); src.push("uniform float logDepthBufFC;"); src.push("in float vFragDepth;"); } if (clipping) { src.push("in vec4 vWorldPosition;"); src.push("flat in uint vFlags2;"); for (let i = 0, len = scene._sectionPlanesState.getNumAllocatedSectionPlanes(); i < len; i++) { src.push("uniform bool sectionPlaneActive" + i + ";"); src.push("uniform vec3 sectionPlanePos" + i + ";"); src.push("uniform vec3 sectionPlaneDir" + i + ";"); } } src.push("void main(void) {"); if (clipping) { src.push(" bool clippable = vFlags2 > 0u;"); src.push(" if (clippable) {"); src.push(" float dist = 0.0;"); for (let i = 0, len = scene._sectionPlanesState.getNumAllocatedSectionPlanes(); i < len; i++) { src.push("if (sectionPlaneActive" + i + ") {"); src.push(" dist += clamp(dot(-sectionPlaneDir" + i + ".xyz, vWorldPosition.xyz - sectionPlanePos" + i + ".xyz), 0.0, 1000.0);"); src.push("}"); } src.push(" if (dist > 0.0) { "); src.push(" discard;"); src.push(" }"); src.push("}"); } if (scene.logarithmicDepthBufferEnabled) { src.push(" gl_FragDepth = isPerspective == 0.0 ? gl_FragCoord.z : log2( vFragDepth ) * logDepthBufFC * 0.5;"); //src.push(" gl_FragDepth = log2( vFragDepth ) * logDepthBufFC * 0.5;"); } src.push("float fragCoordZ = 0.5 * vHighPrecisionZW[0] / vHighPrecisionZW[1] + 0.5;"); src.push(" outColor = vec4(vec3(1.0 - fragCoordZ), 1.0); "); src.push("}"); return src; } webglContextRestored() { this._program = null; } destroy() { if (this._program) { this._program.destroy(); } this._program = null; } } const tempVec3a$f = math.vec3(); const tempVec3b$b = math.vec3(); const tempVec3c$9 = math.vec3(); math.vec3(); const tempMat4a$2 = math.mat4(); /** * @private */ class DTXTrianglesNormalsRenderer { constructor(scene) { this._scene = scene; this._hash = this._getHash(); this._allocate(); } getValid() { return this._hash === this._getHash(); }; _getHash() { return this._scene._sectionPlanesState.getHash(); } drawLayer(frameCtx, dataTextureLayer, renderPass) { const model = dataTextureLayer.model; const scene = model.scene; const camera = scene.camera; const gl = scene.canvas.gl; const state = dataTextureLayer._state; const origin = dataTextureLayer._state.origin; const {position, rotationMatrix} = model; const viewMatrix = camera.viewMatrix; if (!this._program) { this._allocate(dataTextureLayer); if (this.errors) { return; } } if (frameCtx.lastProgramId !== this._program.id) { frameCtx.lastProgramId = this._program.id; this._bindProgram(dataTextureLayer); } let rtcViewMatrix; let rtcCameraEye; const gotOrigin = (origin[0] !== 0 || origin[1] !== 0 || origin[2] !== 0); const gotPosition = (position[0] !== 0 || position[1] !== 0 || position[2] !== 0); if (gotOrigin || gotPosition) { const rtcOrigin = tempVec3a$f; if (gotOrigin) { const rotatedOrigin = tempVec3b$b; math.transformPoint3(rotationMatrix, origin, rotatedOrigin); rtcOrigin[0] = rotatedOrigin[0]; rtcOrigin[1] = rotatedOrigin[1]; rtcOrigin[2] = rotatedOrigin[2]; } else { rtcOrigin[0] = 0; rtcOrigin[1] = 0; rtcOrigin[2] = 0; } rtcOrigin[0] += position[0]; rtcOrigin[1] += position[1]; rtcOrigin[2] += position[2]; rtcViewMatrix = createRTCViewMat(viewMatrix, rtcOrigin, tempMat4a$2); rtcCameraEye = tempVec3c$9; rtcCameraEye[0] = camera.eye[0] - rtcOrigin[0]; rtcCameraEye[1] = camera.eye[1] - rtcOrigin[1]; rtcCameraEye[2] = camera.eye[2] - rtcOrigin[2]; } else { rtcViewMatrix = viewMatrix; rtcCameraEye = camera.eye; } gl.uniform1i(this._uRenderPass, renderPass); gl.uniformMatrix4fv(this._uWorldMatrix, false, rotationMatrix); gl.uniformMatrix4fv(this._uViewMatrix, false, rtcViewMatrix); gl.uniformMatrix4fv(this._uProjMatrix, false, camera.projMatrix); gl.uniformMatrix4fv(this._uViewNormalMatrix, false, camera.viewNormalMatrix); gl.uniformMatrix4fv(this._uWorldNormalMatrix, false, model.worldNormalMatrix); const numAllocatedSectionPlanes = scene._sectionPlanesState.getNumAllocatedSectionPlanes(); const numSectionPlanes = scene._sectionPlanesState.sectionPlanes.length; if (numAllocatedSectionPlanes > 0) { const sectionPlanes = scene._sectionPlanesState.sectionPlanes; const baseIndex = dataTextureLayer.layerIndex * numSectionPlanes; const renderFlags = model.renderFlags; for (let sectionPlaneIndex = 0; sectionPlaneIndex < numAllocatedSectionPlanes; sectionPlaneIndex++) { const sectionPlaneUniforms = this._uSectionPlanes[sectionPlaneIndex]; if (sectionPlaneUniforms) { if (sectionPlaneIndex < numSectionPlanes) { const active = renderFlags.sectionPlanesActivePerLayer[baseIndex + sectionPlaneIndex]; gl.uniform1i(sectionPlaneUniforms.active, active ? 1 : 0); if (active) { const sectionPlane = sectionPlanes[sectionPlaneIndex]; if (origin) { const rtcSectionPlanePos = getPlaneRTCPos(sectionPlane.dist, sectionPlane.dir, origin, tempVec3a$f, model.matrix); gl.uniform3fv(sectionPlaneUniforms.pos, rtcSectionPlanePos); } else { gl.uniform3fv(sectionPlaneUniforms.pos, sectionPlane.pos); } gl.uniform3fv(sectionPlaneUniforms.dir, sectionPlane.dir); } } else { gl.uniform1i(sectionPlaneUniforms.active, 0); } } } } gl.uniformMatrix4fv(this._uPositionsDecodeMatrix, false, dataTextureLayer._state.objectDecodeAndInstanceMatrix); this._aPosition.bindArrayBuffer(state.positionsBuf); this._aOffset.bindArrayBuffer(state.offsetsBuf); this._aNormal.bindArrayBuffer(state.normalsBuf); this._aColor.bindArrayBuffer(state.colorsBuf);// Needed for masking out transparent entities using alpha channel this._aFlags.bindArrayBuffer(state.flagsBuf); if (this._aFlags2) { this._aFlags2.bindArrayBuffer(state.flags2Buf); } state.indicesBuf.bind(); gl.drawElements(gl.TRIANGLES, state.indicesBuf.numItems, state.indicesBuf.itemType, 0); } _allocate() { const scene = this._scene; const gl = scene.canvas.gl; this._program = new Program(gl, this._buildShader()); if (this._program.errors) { this.errors = this._program.errors; return; } const program = this._program; this._uRenderPass = program.getLocation("renderPass"); this._uPositionsDecodeMatrix = program.getLocation("objectDecodeAndInstanceMatrix"); this._uWorldMatrix = program.getLocation("worldMatrix"); this._uWorldNormalMatrix = program.getLocation("worldNormalMatrix"); this._uViewMatrix = program.getLocation("viewMatrix"); this._uViewNormalMatrix = program.getLocation("viewNormalMatrix"); this._uProjMatrix = program.getLocation("projMatrix"); this._uSectionPlanes = []; for (let i = 0, len = scene._sectionPlanesState.getNumAllocatedSectionPlanes(); i < len; i++) { this._uSectionPlanes.push({ active: program.getLocation("sectionPlaneActive" + i), pos: program.getLocation("sectionPlanePos" + i), dir: program.getLocation("sectionPlaneDir" + i) }); } this._aPosition = program.getAttribute("position"); this._aOffset = program.getAttribute("offset"); this._aNormal = program.getAttribute("normal"); this._aColor = program.getAttribute("color"); this._aFlags = program.getAttribute("flags"); if (this._aFlags2) { // Won't be in shader when not clipping this._aFlags2 = program.getAttribute("flags2"); } if ( scene.logarithmicDepthBufferEnabled) { this._uLogDepthBufFC = program.getLocation("logDepthBufFC"); } } _bindProgram() { const scene = this._scene; const gl = scene.canvas.gl; const project = scene.camera.project; this._program.bind(); gl.uniformMatrix4fv(this._uProjMatrix, false, project.matrix); if ( scene.logarithmicDepthBufferEnabled) { const logDepthBufFC = 2.0 / (Math.log(project.far + 1.0) / Math.LN2); gl.uniform1f(this._uLogDepthBufFC, logDepthBufFC); } } _buildShader() { return { vertex: this._buildVertexShader(), fragment: this._buildFragmentShader() }; } _buildVertexShader() { const scene = this._scene; const clipping = scene._sectionPlanesState.getNumAllocatedSectionPlanes() > 0; const src = []; src.push("// Batched geometry normals vertex shader"); if (scene.logarithmicDepthBufferEnabled && WEBGL_INFO.SUPPORTED_EXTENSIONS["EXT_frag_depth"]) { src.push("#extension GL_EXT_frag_depth : enable"); } src.push("uniform int renderPass;"); src.push("attribute vec3 position;"); if (scene.entityOffsetsEnabled) { src.push("attribute vec3 offset;"); } src.push("attribute vec3 normal;"); src.push("attribute vec4 color;"); src.push("attribute vec4 flags;"); src.push("attribute vec4 flags2;"); src.push("uniform mat4 worldMatrix;"); src.push("uniform mat4 worldNormalMatrix;"); src.push("uniform mat4 viewMatrix;"); src.push("uniform mat4 projMatrix;"); src.push("uniform mat4 viewNormalMatrix;"); src.push("uniform mat4 objectDecodeAndInstanceMatrix;"); if (scene.logarithmicDepthBufferEnabled) { src.push("uniform float logDepthBufFC;"); if (WEBGL_INFO.SUPPORTED_EXTENSIONS["EXT_frag_depth"]) { src.push("out float vFragDepth;"); } src.push("bool isPerspectiveMatrix(mat4 m) {"); src.push(" return (m[2][3] == - 1.0);"); src.push("}"); src.push("varying float isPerspective;"); } src.push("vec3 octDecode(vec2 oct) {"); src.push(" vec3 v = vec3(oct.xy, 1.0 - abs(oct.x) - abs(oct.y));"); src.push(" if (v.z < 0.0) {"); src.push(" v.xy = (1.0 - abs(v.yx)) * vec2(v.x >= 0.0 ? 1.0 : -1.0, v.y >= 0.0 ? 1.0 : -1.0);"); src.push(" }"); src.push(" return normalize(v);"); src.push("}"); if (clipping) { src.push("out vec4 vWorldPosition;"); src.push("out vec4 vFlags2;"); } src.push("out vec3 vViewNormal;"); src.push("void main(void) {"); // flags.x = NOT_RENDERED | COLOR_OPAQUE | COLOR_TRANSPARENT // renderPass = COLOR_OPAQUE src.push(`if (int(flags.x) != renderPass) {`); src.push(" gl_Position = vec4(0.0, 0.0, 0.0, 0.0);"); src.push(" } else {"); src.push(" vec4 worldPosition = worldMatrix * (objectDecodeAndInstanceMatrix * vec4(position, 1.0)); "); if (scene.entityOffsetsEnabled) { src.push(" worldPosition.xyz = worldPosition.xyz + offset;"); } src.push(" vec4 viewPosition = viewMatrix * worldPosition; "); src.push(" vec4 worldNormal = worldNormalMatrix * vec4(octDecode(normal.xy), 0.0); "); src.push(" vec3 viewNormal = normalize((viewNormalMatrix * worldNormal).xyz);"); if (clipping) { src.push(" vWorldPosition = worldPosition;"); src.push(" vFlags2 = flags2;"); } src.push(" vViewNormal = viewNormal;"); src.push("vec4 clipPos = projMatrix * viewPosition;"); if (scene.logarithmicDepthBufferEnabled) { if (WEBGL_INFO.SUPPORTED_EXTENSIONS["EXT_frag_depth"]) { src.push("vFragDepth = 1.0 + clipPos.w;"); } else { src.push("clipPos.z = log2( max( 1e-6, clipPos.w + 1.0 ) ) * logDepthBufFC - 1.0;"); src.push("clipPos.z *= clipPos.w;"); } src.push("isPerspective = float (isPerspectiveMatrix(projMatrix));"); } src.push("gl_Position = clipPos;"); src.push(" }"); src.push("}"); return src; } _buildFragmentShader() { const scene = this._scene; const clipping = (scene._sectionPlanesState.getNumAllocatedSectionPlanes() > 0); const src = []; src.push("#version 300 es"); src.push("// Batched geometry normals fragment shader"); if (scene.logarithmicDepthBufferEnabled && WEBGL_INFO.SUPPORTED_EXTENSIONS["EXT_frag_depth"]) { src.push("#extension GL_EXT_frag_depth : enable"); } src.push("#ifdef GL_FRAGMENT_PRECISION_HIGH"); src.push("precision highp float;"); src.push("precision highp int;"); src.push("#else"); src.push("precision mediump float;"); src.push("precision mediump int;"); src.push("#endif"); if (scene.logarithmicDepthBufferEnabled && WEBGL_INFO.SUPPORTED_EXTENSIONS["EXT_frag_depth"]) { src.push("in float isPerspective;"); src.push("uniform float logDepthBufFC;"); src.push("in float vFragDepth;"); } if (clipping) { src.push("in vec4 vWorldPosition;"); src.push("in vec4 vFlags2;"); for (let i = 0; i < scene._sectionPlanesState.getNumAllocatedSectionPlanes(); i++) { src.push("uniform bool sectionPlaneActive" + i + ";"); src.push("uniform vec3 sectionPlanePos" + i + ";"); src.push("uniform vec3 sectionPlaneDir" + i + ";"); } } src.push("in vec3 vViewNormal;"); src.push("vec3 packNormalToRGB( const in vec3 normal ) {"); src.push(" return normalize( normal ) * 0.5 + 0.5;"); src.push("}"); src.push("void main(void) {"); if (clipping) { src.push(" bool clippable = (float(vFlags2.x) > 0.0);"); src.push(" if (clippable) {"); src.push(" float dist = 0.0;"); for (var i = 0; i < scene._sectionPlanesState.getNumAllocatedSectionPlanes(); i++) { src.push(" if (sectionPlaneActive" + i + ") {"); src.push(" dist += clamp(dot(-sectionPlaneDir" + i + ".xyz, vWorldPosition.xyz - sectionPlanePos" + i + ".xyz), 0.0, 1000.0);"); src.push(" }"); } src.push(" if (dist > 0.0) { discard; }"); src.push(" }"); } if (scene.logarithmicDepthBufferEnabled && WEBGL_INFO.SUPPORTED_EXTENSIONS["EXT_frag_depth"]) { src.push(" gl_FragDepthEXT = isPerspective == 0.0 ? gl_FragCoord.z : log2( vFragDepth ) * logDepthBufFC * 0.5;"); } src.push(" gl_FragColor = vec4(packNormalToRGB(vViewNormal), 1.0); "); src.push("}"); return src; } webglContextRestored() { this._program = null; } destroy() { if (this._program) { this._program.destroy(); } this._program = null; } } const tempVec3a$e = math.vec3(); const tempVec3b$a = math.vec3(); const tempVec3c$8 = math.vec3(); math.vec3(); math.vec4(); const tempMat4a$1 = math.mat4(); /** * @private */ class DTXTrianglesPickNormalsFlatRenderer { constructor(scene, withSAO) { this._scene = scene; this._withSAO = withSAO; this._hash = this._getHash(); this._allocate(); } getValid() { return this._hash === this._getHash(); }; _getHash() { const scene = this._scene; return [scene._lightsState.getHash(), scene._sectionPlanesState.getHash(), (this._withSAO ? "sao" : "nosao")].join(";"); } drawLayer(frameCtx, dataTextureLayer, renderPass) { const scene = this._scene; const camera = scene.camera; const model = dataTextureLayer.model; const gl = scene.canvas.gl; const state = dataTextureLayer._state; const textureState = state.textureState; const origin = dataTextureLayer._state.origin; const {position, rotationMatrix} = model; if (!this._program) { this._allocate(); if (this.errors) { return; } } if (frameCtx.lastProgramId !== this._program.id) { frameCtx.lastProgramId = this._program.id; this._bindProgram(frameCtx, state); } const viewMatrix = frameCtx.pickViewMatrix || camera.viewMatrix; const projMatrix = frameCtx.pickProjMatrix || camera.projMatrix; const eye = frameCtx.pickOrigin || camera.eye; const far = frameCtx.pickProjMatrix ? frameCtx.pickZFar : camera.project.far; textureState.bindCommonTextures( this._program, this.uTexturePerObjectPositionsDecodeMatrix, this._uTexturePerVertexIdCoordinates, this.uTexturePerObjectColorsAndFlags, this._uTexturePerObjectMatrix ); let rtcViewMatrix; let rtcCameraEye; const gotOrigin = (origin[0] !== 0 || origin[1] !== 0 || origin[2] !== 0); const gotPosition = (position[0] !== 0 || position[1] !== 0 || position[2] !== 0); if (gotOrigin || gotPosition) { const rtcOrigin = tempVec3a$e; if (gotOrigin) { const rotatedOrigin = math.transformPoint3(rotationMatrix, origin, tempVec3b$a); rtcOrigin[0] = rotatedOrigin[0]; rtcOrigin[1] = rotatedOrigin[1]; rtcOrigin[2] = rotatedOrigin[2]; } else { rtcOrigin[0] = 0; rtcOrigin[1] = 0; rtcOrigin[2] = 0; } rtcOrigin[0] += position[0]; rtcOrigin[1] += position[1]; rtcOrigin[2] += position[2]; rtcViewMatrix = createRTCViewMat(viewMatrix, rtcOrigin, tempMat4a$1); rtcCameraEye = tempVec3c$8; rtcCameraEye[0] = eye[0] - rtcOrigin[0]; rtcCameraEye[1] = eye[1] - rtcOrigin[1]; rtcCameraEye[2] = eye[2] - rtcOrigin[2]; } else { rtcViewMatrix = viewMatrix; rtcCameraEye = eye; } gl.uniform2fv(this._uPickClipPos, frameCtx.pickClipPos); gl.uniform2f(this._uDrawingBufferSize, gl.drawingBufferWidth, gl.drawingBufferHeight); gl.uniformMatrix4fv(this._uSceneModelMatrix, false, rotationMatrix); gl.uniformMatrix4fv(this._uViewMatrix, false, rtcViewMatrix); gl.uniformMatrix4fv(this._uProjMatrix, false, projMatrix); gl.uniform3fv(this._uCameraEyeRtc, rtcCameraEye); gl.uniform1i(this._uRenderPass, renderPass); if (scene.logarithmicDepthBufferEnabled) { const logDepthBufFC = 2.0 / (Math.log(far + 1.0) / Math.LN2); gl.uniform1f(this._uLogDepthBufFC, logDepthBufFC); } const numAllocatedSectionPlanes = scene._sectionPlanesState.getNumAllocatedSectionPlanes(); const numSectionPlanes = scene._sectionPlanesState.sectionPlanes.length; if (numAllocatedSectionPlanes > 0) { const sectionPlanes = scene._sectionPlanesState.sectionPlanes; const baseIndex = dataTextureLayer.layerIndex * numSectionPlanes; const renderFlags = model.renderFlags; for (let sectionPlaneIndex = 0; sectionPlaneIndex < numAllocatedSectionPlanes; sectionPlaneIndex++) { const sectionPlaneUniforms = this._uSectionPlanes[sectionPlaneIndex]; if (sectionPlaneUniforms) { if (sectionPlaneIndex < numSectionPlanes) { const active = renderFlags.sectionPlanesActivePerLayer[baseIndex + sectionPlaneIndex]; gl.uniform1i(sectionPlaneUniforms.active, active ? 1 : 0); if (active) { const sectionPlane = sectionPlanes[sectionPlaneIndex]; if (origin) { const rtcSectionPlanePos = getPlaneRTCPos(sectionPlane.dist, sectionPlane.dir, origin, tempVec3a$e, model.matrix); gl.uniform3fv(sectionPlaneUniforms.pos, rtcSectionPlanePos); } else { gl.uniform3fv(sectionPlaneUniforms.pos, sectionPlane.pos); } gl.uniform3fv(sectionPlaneUniforms.dir, sectionPlane.dir); } } else { gl.uniform1i(sectionPlaneUniforms.active, 0); } } } } if (state.numIndices8Bits > 0) { textureState.bindTriangleIndicesTextures( this._program, this._uTexturePerPolygonIdPortionIds, this._uTexturePerPolygonIdIndices, 8 // 8 bits indices ); gl.drawArrays(gl.TRIANGLES, 0, state.numIndices8Bits); } if (state.numIndices16Bits > 0) { textureState.bindTriangleIndicesTextures( this._program, this._uTexturePerPolygonIdPortionIds, this._uTexturePerPolygonIdIndices, 16 // 16 bits indices ); gl.drawArrays(gl.TRIANGLES, 0, state.numIndices16Bits); } if (state.numIndices32Bits > 0) { textureState.bindTriangleIndicesTextures( this._program, this._uTexturePerPolygonIdPortionIds, this._uTexturePerPolygonIdIndices, 32 // 32 bits indices ); gl.drawArrays(gl.TRIANGLES, 0, state.numIndices32Bits); } frameCtx.drawElements++; } _allocate() { const scene = this._scene; const gl = scene.canvas.gl; this._program = new Program(gl, this._buildShader()); if (this._program.errors) { this.errors = this._program.errors; return; } const program = this._program; this._uRenderPass = program.getLocation("renderPass"); this._uPickInvisible = program.getLocation("pickInvisible"); this._uPickClipPos = program.getLocation("pickClipPos"); this._uDrawingBufferSize = program.getLocation("drawingBufferSize"); this._uSceneModelMatrix = program.getLocation("sceneModelMatrix"); this._uViewMatrix = program.getLocation("viewMatrix"); this._uProjMatrix = program.getLocation("projMatrix"); this._uSectionPlanes = []; for (let i = 0, len = scene._sectionPlanesState.getNumAllocatedSectionPlanes(); i < len; i++) { this._uSectionPlanes.push({ active: program.getLocation("sectionPlaneActive" + i), pos: program.getLocation("sectionPlanePos" + i), dir: program.getLocation("sectionPlaneDir" + i) }); } if (scene.logarithmicDepthBufferEnabled) { this._uLogDepthBufFC = program.getLocation("logDepthBufFC"); } this.uTexturePerObjectPositionsDecodeMatrix = "uObjectPerObjectPositionsDecodeMatrix"; this.uTexturePerObjectColorsAndFlags = "uObjectPerObjectColorsAndFlags"; this._uTexturePerVertexIdCoordinates = "uTexturePerVertexIdCoordinates"; this._uTexturePerPolygonIdNormals = "uTexturePerPolygonIdNormals"; this._uTexturePerPolygonIdIndices = "uTexturePerPolygonIdIndices"; this._uTexturePerPolygonIdPortionIds = "uTexturePerPolygonIdPortionIds"; this._uTexturePerObjectMatrix = "uTexturePerObjectMatrix"; this._uCameraEyeRtc = program.getLocation("uCameraEyeRtc"); } _bindProgram(frameCtx) { const scene = this._scene; const gl = scene.canvas.gl; const program = this._program; const project = scene.camera.project; program.bind(); if (scene.logarithmicDepthBufferEnabled) { const logDepthBufFC = 2.0 / (Math.log(project.far + 1.0) / Math.LN2); gl.uniform1f(this._uLogDepthBufFC, logDepthBufFC); } } _buildShader() { return { vertex: this._buildVertexShader(), fragment: this._buildFragmentShader() }; } _buildVertexShader() { const scene = this._scene; const clipping = scene._sectionPlanesState.getNumAllocatedSectionPlanes() > 0; const src = []; src.push("#version 300 es"); src.push("// trianglesDatatextureNormalsRenderer vertex shader"); src.push("#ifdef GL_FRAGMENT_PRECISION_HIGH"); src.push("precision highp float;"); src.push("precision highp int;"); src.push("precision highp usampler2D;"); src.push("precision highp isampler2D;"); src.push("precision highp sampler2D;"); src.push("#else"); src.push("precision mediump float;"); src.push("precision mediump int;"); src.push("precision mediump usampler2D;"); src.push("precision mediump isampler2D;"); src.push("precision mediump sampler2D;"); src.push("#endif"); src.push("uniform int renderPass;"); if (scene.entityOffsetsEnabled) { src.push("in vec3 offset;"); } src.push("uniform mat4 sceneModelMatrix;"); src.push("uniform mat4 viewMatrix;"); src.push("uniform mat4 projMatrix;"); src.push("uniform highp sampler2D uObjectPerObjectPositionsDecodeMatrix;"); src.push("uniform lowp usampler2D uObjectPerObjectColorsAndFlags;"); src.push("uniform highp sampler2D uTexturePerObjectMatrix;"); src.push("uniform mediump usampler2D uTexturePerVertexIdCoordinates;"); src.push("uniform highp usampler2D uTexturePerPolygonIdIndices;"); src.push("uniform mediump usampler2D uTexturePerPolygonIdPortionIds;"); src.push("uniform vec3 uCameraEyeRtc;"); src.push("vec3 positions[3];"); if (scene.logarithmicDepthBufferEnabled) { src.push("uniform float logDepthBufFC;"); src.push("out float vFragDepth;"); src.push("out float isPerspective;"); } src.push("uniform vec2 pickClipPos;"); src.push("uniform vec2 drawingBufferSize;"); src.push("vec4 remapClipPos(vec4 clipPos) {"); src.push(" clipPos.xy /= clipPos.w;"); src.push(` clipPos.xy = (clipPos.xy - pickClipPos) * drawingBufferSize;`); src.push(" clipPos.xy *= clipPos.w;"); src.push(" return clipPos;"); src.push("}"); src.push("bool isPerspectiveMatrix(mat4 m) {"); src.push(" return (m[2][3] == - 1.0);"); src.push("}"); src.push("out vec4 vWorldPosition;"); if (clipping) { src.push("flat out uint vFlags2;"); } src.push("void main(void) {"); // constants src.push("int polygonIndex = gl_VertexID / 3;"); // get packed object-id src.push("int h_packed_object_id_index = (polygonIndex >> 3) & 4095;"); src.push("int v_packed_object_id_index = (polygonIndex >> 3) >> 12;"); src.push("int objectIndex = int(texelFetch(uTexturePerPolygonIdPortionIds, ivec2(h_packed_object_id_index, v_packed_object_id_index), 0).r);"); src.push("ivec2 objectIndexCoords = ivec2(objectIndex % 512, objectIndex / 512);"); // get flags & flags2 src.push("uvec4 flags = texelFetch (uObjectPerObjectColorsAndFlags, ivec2(objectIndexCoords.x*8+2, objectIndexCoords.y), 0);"); src.push("uvec4 flags2 = texelFetch (uObjectPerObjectColorsAndFlags, ivec2(objectIndexCoords.x*8+3, objectIndexCoords.y), 0);"); // pickFlag = NOT_RENDERED | PICK // renderPass = PICK src.push(`if (int(flags.w) != renderPass) {`); src.push(" gl_Position = vec4(3.0, 3.0, 3.0, 1.0);"); // Cull vertex src.push(" return;"); // Cull vertex src.push("} else {"); // get vertex base src.push("ivec4 packedVertexBase = ivec4(texelFetch (uObjectPerObjectColorsAndFlags, ivec2(objectIndexCoords.x*8+4, objectIndexCoords.y), 0));"); src.push("ivec4 packedIndexBaseOffset = ivec4(texelFetch (uObjectPerObjectColorsAndFlags, ivec2(objectIndexCoords.x*8+5, objectIndexCoords.y), 0));"); src.push("int indexBaseOffset = (packedIndexBaseOffset.r << 24) + (packedIndexBaseOffset.g << 16) + (packedIndexBaseOffset.b << 8) + packedIndexBaseOffset.a;"); src.push("int h_index = (polygonIndex - indexBaseOffset) & 4095;"); src.push("int v_index = (polygonIndex - indexBaseOffset) >> 12;"); src.push("ivec3 vertexIndices = ivec3(texelFetch(uTexturePerPolygonIdIndices, ivec2(h_index, v_index), 0));"); src.push("ivec3 uniqueVertexIndexes = vertexIndices + (packedVertexBase.r << 24) + (packedVertexBase.g << 16) + (packedVertexBase.b << 8) + packedVertexBase.a;"); src.push("ivec3 indexPositionH = uniqueVertexIndexes & 4095;"); src.push("ivec3 indexPositionV = uniqueVertexIndexes >> 12;"); src.push("mat4 objectInstanceMatrix = mat4 (texelFetch (uTexturePerObjectMatrix, ivec2(objectIndexCoords.x*4+0, objectIndexCoords.y), 0), texelFetch (uTexturePerObjectMatrix, ivec2(objectIndexCoords.x*4+1, objectIndexCoords.y), 0), texelFetch (uTexturePerObjectMatrix, ivec2(objectIndexCoords.x*4+2, objectIndexCoords.y), 0), texelFetch (uTexturePerObjectMatrix, ivec2(objectIndexCoords.x*4+3, objectIndexCoords.y), 0));"); src.push("mat4 objectDecodeAndInstanceMatrix = objectInstanceMatrix * mat4 (texelFetch (uObjectPerObjectPositionsDecodeMatrix, ivec2(objectIndexCoords.x*4+0, objectIndexCoords.y), 0), texelFetch (uObjectPerObjectPositionsDecodeMatrix, ivec2(objectIndexCoords.x*4+1, objectIndexCoords.y), 0), texelFetch (uObjectPerObjectPositionsDecodeMatrix, ivec2(objectIndexCoords.x*4+2, objectIndexCoords.y), 0), texelFetch (uObjectPerObjectPositionsDecodeMatrix, ivec2(objectIndexCoords.x*4+3, objectIndexCoords.y), 0));"); src.push("uint solid = texelFetch (uObjectPerObjectColorsAndFlags, ivec2(objectIndexCoords.x*8+7, objectIndexCoords.y), 0).r;"); // get position src.push("positions[0] = vec3(texelFetch(uTexturePerVertexIdCoordinates, ivec2(indexPositionH.r, indexPositionV.r), 0));"); src.push("positions[1] = vec3(texelFetch(uTexturePerVertexIdCoordinates, ivec2(indexPositionH.g, indexPositionV.g), 0));"); src.push("positions[2] = vec3(texelFetch(uTexturePerVertexIdCoordinates, ivec2(indexPositionH.b, indexPositionV.b), 0));"); // get color src.push("uvec4 color = texelFetch (uObjectPerObjectColorsAndFlags, ivec2(objectIndexCoords.x*8+0, objectIndexCoords.y), 0);"); src.push(`if (color.a == 0u) {`); src.push(" gl_Position = vec4(3.0, 3.0, 3.0, 1.0);"); // Cull vertex src.push(" return;"); src.push("};"); // get normal src.push("vec3 normal = normalize(cross(positions[2] - positions[0], positions[1] - positions[0]));"); src.push("vec3 position;"); src.push("position = positions[gl_VertexID % 3];"); src.push("vec3 viewNormal = -normalize((transpose(inverse(viewMatrix*objectDecodeAndInstanceMatrix)) * vec4(normal,1)).xyz);"); // when the geometry is not solid, if needed, flip the triangle winding src.push("if (solid != 1u) {"); src.push("if (isPerspectiveMatrix(projMatrix)) {"); src.push("vec3 uCameraEyeRtcInQuantizedSpace = (inverse(sceneModelMatrix * objectDecodeAndInstanceMatrix) * vec4(uCameraEyeRtc, 1)).xyz;"); // src.push("vColor = vec4(vec3(1, -1, 0)*dot(normalize(position.xyz - uCameraEyeRtcInQuantizedSpace), normal), 1);") src.push("if (dot(position.xyz - uCameraEyeRtcInQuantizedSpace, normal) < 0.0) {"); src.push("position = positions[2 - (gl_VertexID % 3)];"); src.push("viewNormal = -viewNormal;"); src.push("}"); src.push("} else {"); src.push("vec3 viewNormal = -normalize((transpose(inverse(viewMatrix*objectDecodeAndInstanceMatrix)) * vec4(normal,1)).xyz);"); src.push("if (viewNormal.z < 0.0) {"); src.push("position = positions[2 - (gl_VertexID % 3)];"); src.push("}"); src.push("}"); src.push("}"); src.push("vec4 worldPosition = sceneModelMatrix * (objectDecodeAndInstanceMatrix * vec4(position, 1.0)); "); src.push("vec4 viewPosition = viewMatrix * worldPosition; "); src.push("vec4 clipPos = projMatrix * viewPosition;"); if (scene.logarithmicDepthBufferEnabled) { src.push("vFragDepth = 1.0 + clipPos.w;"); src.push("isPerspective = float (isPerspectiveMatrix(projMatrix));"); } src.push("vWorldPosition = worldPosition;"); if (clipping) { src.push("vFlags2 = flags2.r;"); } src.push("gl_Position = remapClipPos(clipPos);"); src.push("}"); src.push("}"); return src; } _buildFragmentShader() { const scene = this._scene; const clipping = scene._sectionPlanesState.getNumAllocatedSectionPlanes() > 0; const src = []; src.push('#version 300 es'); src.push("// TrianglesDataTexturePickNormalsRenderer fragment shader"); src.push("#ifdef GL_FRAGMENT_PRECISION_HIGH"); src.push("precision highp float;"); src.push("precision highp int;"); src.push("#else"); src.push("precision mediump float;"); src.push("precision mediump int;"); src.push("#endif"); if (scene.logarithmicDepthBufferEnabled) { src.push("in float isPerspective;"); src.push("uniform float logDepthBufFC;"); src.push("in float vFragDepth;"); } src.push("in vec4 vWorldPosition;"); if (clipping) { src.push("flat in uint vFlags2;"); for (let i = 0, len = scene._sectionPlanesState.getNumAllocatedSectionPlanes(); i < len; i++) { src.push("uniform bool sectionPlaneActive" + i + ";"); src.push("uniform vec3 sectionPlanePos" + i + ";"); src.push("uniform vec3 sectionPlaneDir" + i + ";"); } } src.push("out highp ivec4 outNormal;"); src.push("void main(void) {"); if (clipping) { src.push(" bool clippable = vFlags2 > 0u;"); src.push(" if (clippable) {"); src.push(" float dist = 0.0;"); for (let i = 0, len = scene._sectionPlanesState.getNumAllocatedSectionPlanes(); i < len; i++) { src.push("if (sectionPlaneActive" + i + ") {"); src.push(" dist += clamp(dot(-sectionPlaneDir" + i + ".xyz, vWorldPosition.xyz - sectionPlanePos" + i + ".xyz), 0.0, 1000.0);"); src.push("}"); } src.push(" if (dist > 0.0) { "); src.push(" discard;"); src.push(" }"); src.push("}"); } if (scene.logarithmicDepthBufferEnabled) { src.push(" gl_FragDepth = isPerspective == 0.0 ? gl_FragCoord.z : log2( vFragDepth ) * logDepthBufFC * 0.5;"); } src.push(" vec3 xTangent = dFdx( vWorldPosition.xyz );"); src.push(" vec3 yTangent = dFdy( vWorldPosition.xyz );"); src.push(" vec3 worldNormal = normalize( cross( xTangent, yTangent ) );"); src.push(` outNormal = ivec4(worldNormal * float(${math.MAX_INT}), 1.0);`); src.push("}"); return src; } webglContextRestored() { this._program = null; } destroy() { if (this._program) { this._program.destroy(); } this._program = null; } } /** * @private */ class DTXTrianglesRenderers { constructor(scene) { this._scene = scene; } _compile() { if (this._colorRenderer && (!this._colorRenderer.getValid())) { this._colorRenderer.destroy(); this._colorRenderer = null; } if (this._colorRendererWithSAO && (!this._colorRendererWithSAO.getValid())) { this._colorRendererWithSAO.destroy(); this._colorRendererWithSAO = null; } if (this._flatColorRenderer && (!this._flatColorRenderer.getValid())) { this._flatColorRenderer.destroy(); this._flatColorRenderer = null; } if (this._flatColorRendererWithSAO && (!this._flatColorRendererWithSAO.getValid())) { this._flatColorRendererWithSAO.destroy(); this._flatColorRendererWithSAO = null; } if (this._colorQualityRendererWithSAO && (!this._colorQualityRendererWithSAO.getValid())) { this._colorQualityRendererWithSAO.destroy(); this._colorQualityRendererWithSAO = null; } if (this._depthRenderer && (!this._depthRenderer.getValid())) { this._depthRenderer.destroy(); this._depthRenderer = null; } if (this._normalsRenderer && (!this._normalsRenderer.getValid())) { this._normalsRenderer.destroy(); this._normalsRenderer = null; } if (this._silhouetteRenderer && (!this._silhouetteRenderer.getValid())) { this._silhouetteRenderer.destroy(); this._silhouetteRenderer = null; } if (this._edgesRenderer && (!this._edgesRenderer.getValid())) { this._edgesRenderer.destroy(); this._edgesRenderer = null; } if (this._edgesColorRenderer && (!this._edgesColorRenderer.getValid())) { this._edgesColorRenderer.destroy(); this._edgesColorRenderer = null; } if (this._pickMeshRenderer && (!this._pickMeshRenderer.getValid())) { this._pickMeshRenderer.destroy(); this._pickMeshRenderer = null; } if (this._pickDepthRenderer && (!this._pickDepthRenderer.getValid())) { this._pickDepthRenderer.destroy(); this._pickDepthRenderer = null; } if (this._snapRenderer && (!this._snapRenderer.getValid())) { this._snapRenderer.destroy(); this._snapRenderer = null; } if (this._snapInitRenderer && (!this._snapInitRenderer.getValid())) { this._snapInitRenderer.destroy(); this._snapInitRenderer = null; } if (this._pickNormalsRenderer && this._pickNormalsRenderer.getValid() === false) { this._pickNormalsRenderer.destroy(); this._pickNormalsRenderer = null; } if (this._pickNormalsFlatRenderer && this._pickNormalsFlatRenderer.getValid() === false) { this._pickNormalsFlatRenderer.destroy(); this._pickNormalsFlatRenderer = null; } if (this._occlusionRenderer && this._occlusionRenderer.getValid() === false) { this._occlusionRenderer.destroy(); this._occlusionRenderer = null; } } eagerCreateRenders() { // Pre-initialize certain renderers that would otherwise be lazy-initialised // on user interaction, such as picking or emphasis, so that there is no delay // when user first begins interacting with the viewer. if (!this._silhouetteRenderer) { // Used for highlighting and selection this._silhouetteRenderer = new DTXTrianglesSilhouetteRenderer(this._scene); } if (!this._pickMeshRenderer) { this._pickMeshRenderer = new DTXTrianglesPickMeshRenderer(this._scene); } if (!this._pickDepthRenderer) { this._pickDepthRenderer = new DTXTrianglesPickDepthRenderer(this._scene); } if (!this._pickNormalsRenderer) { this._pickNormalsRenderer = new DTXTrianglesPickNormalsFlatRenderer(this._scene); } if (!this._snapRenderer) { this._snapRenderer = new DTXTrianglesSnapRenderer(this._scene); } if (!this._snapInitRenderer) { this._snapInitRenderer = new DTXTrianglesSnapInitRenderer(this._scene); } if (!this._snapRenderer) { this._snapRenderer = new DTXTrianglesSnapRenderer(this._scene); } } get colorRenderer() { if (!this._colorRenderer) { this._colorRenderer = new DTXTrianglesColorRenderer(this._scene, false); } return this._colorRenderer; } get colorRendererWithSAO() { if (!this._colorRendererWithSAO) { this._colorRendererWithSAO = new DTXTrianglesColorRenderer(this._scene, true); } return this._colorRendererWithSAO; } get colorQualityRendererWithSAO() { // if (!this._colorQualityRendererWithSAO) { // this._colorQualityRendererWithSAO = new TrianglesDataTextureColorQualityRenderer(this._scene, true); // } return this._colorQualityRendererWithSAO; } get silhouetteRenderer() { if (!this._silhouetteRenderer) { this._silhouetteRenderer = new DTXTrianglesSilhouetteRenderer(this._scene); } return this._silhouetteRenderer; } get depthRenderer() { if (!this._depthRenderer) { this._depthRenderer = new DTXTrianglesDepthRenderer(this._scene); } return this._depthRenderer; } get normalsRenderer() { if (!this._normalsRenderer) { this._normalsRenderer = new DTXTrianglesNormalsRenderer(this._scene); } return this._normalsRenderer; } get edgesRenderer() { if (!this._edgesRenderer) { this._edgesRenderer = new DTXTrianglesEdgesRenderer(this._scene); } return this._edgesRenderer; } get edgesColorRenderer() { if (!this._edgesColorRenderer) { this._edgesColorRenderer = new DTXTrianglesEdgesColorRenderer(this._scene); } return this._edgesColorRenderer; } get pickMeshRenderer() { if (!this._pickMeshRenderer) { this._pickMeshRenderer = new DTXTrianglesPickMeshRenderer(this._scene); } return this._pickMeshRenderer; } get pickNormalsRenderer() { if (!this._pickNormalsRenderer) { this._pickNormalsRenderer = new DTXTrianglesPickNormalsFlatRenderer(this._scene); } return this._pickNormalsRenderer; } get pickNormalsFlatRenderer() { if (!this._pickNormalsFlatRenderer) { this._pickNormalsFlatRenderer = new DTXTrianglesPickNormalsFlatRenderer(this._scene); } return this._pickNormalsFlatRenderer; } get pickDepthRenderer() { if (!this._pickDepthRenderer) { this._pickDepthRenderer = new DTXTrianglesPickDepthRenderer(this._scene); } return this._pickDepthRenderer; } get snapRenderer() { if (!this._snapRenderer) { this._snapRenderer = new DTXTrianglesSnapRenderer(this._scene); } return this._snapRenderer; } get snapInitRenderer() { if (!this._snapInitRenderer) { this._snapInitRenderer = new DTXTrianglesSnapInitRenderer(this._scene); } return this._snapInitRenderer; } get occlusionRenderer() { if (!this._occlusionRenderer) { this._occlusionRenderer = new DTXTrianglesOcclusionRenderer(this._scene); } return this._occlusionRenderer; } _destroy() { if (this._colorRenderer) { this._colorRenderer.destroy(); } if (this._colorRendererWithSAO) { this._colorRendererWithSAO.destroy(); } if (this._flatColorRenderer) { this._flatColorRenderer.destroy(); } if (this._flatColorRendererWithSAO) { this._flatColorRendererWithSAO.destroy(); } if (this._colorQualityRendererWithSAO) { this._colorQualityRendererWithSAO.destroy(); } if (this._depthRenderer) { this._depthRenderer.destroy(); } if (this._normalsRenderer) { this._normalsRenderer.destroy(); } if (this._silhouetteRenderer) { this._silhouetteRenderer.destroy(); } if (this._edgesRenderer) { this._edgesRenderer.destroy(); } if (this._edgesColorRenderer) { this._edgesColorRenderer.destroy(); } if (this._pickMeshRenderer) { this._pickMeshRenderer.destroy(); } if (this._pickDepthRenderer) { this._pickDepthRenderer.destroy(); } if (this._snapRenderer) { this._snapRenderer.destroy(); } if (this._snapInitRenderer) { this._snapInitRenderer.destroy(); } if (this._pickNormalsRenderer) { this._pickNormalsRenderer.destroy(); } if (this._pickNormalsFlatRenderer) { this._pickNormalsFlatRenderer.destroy(); } if (this._occlusionRenderer) { this._occlusionRenderer.destroy(); } } } const cachdRenderers = {}; /** * @private */ function getRenderers(scene) { const sceneId = scene.id; let dataTextureRenderers = cachdRenderers[sceneId]; if (!dataTextureRenderers) { dataTextureRenderers = new DTXTrianglesRenderers(scene); cachdRenderers[sceneId] = dataTextureRenderers; dataTextureRenderers._compile(); dataTextureRenderers.eagerCreateRenders(); scene.on("compile", () => { dataTextureRenderers._compile(); dataTextureRenderers.eagerCreateRenders(); }); scene.on("destroyed", () => { delete cachdRenderers[sceneId]; dataTextureRenderers._destroy(); }); } return dataTextureRenderers; } /** * @private */ class DTXTrianglesBuffer { constructor() { this.positionsCompressed = []; this.lenPositionsCompressed = 0; this.metallicRoughness = []; this.indices8Bits = []; this.lenIndices8Bits = 0; this.indices16Bits = []; this.lenIndices16Bits = 0; this.indices32Bits = []; this.lenIndices32Bits = 0; this.edgeIndices8Bits = []; this.lenEdgeIndices8Bits = 0; this.edgeIndices16Bits = []; this.lenEdgeIndices16Bits = 0; this.edgeIndices32Bits = []; this.lenEdgeIndices32Bits = 0; this.perObjectColors = []; this.perObjectPickColors = []; this.perObjectSolid = []; this.perObjectOffsets = []; this.perObjectPositionsDecodeMatrices = []; this.perObjectInstancePositioningMatrices = []; this.perObjectVertexBases = []; this.perObjectIndexBaseOffsets = []; this.perObjectEdgeIndexBaseOffsets = []; this.perTriangleNumberPortionId8Bits = []; this.perTriangleNumberPortionId16Bits = []; this.perTriangleNumberPortionId32Bits = []; this.perEdgeNumberPortionId8Bits = []; this.perEdgeNumberPortionId16Bits = []; this.perEdgeNumberPortionId32Bits = []; } } // Imports used to complete the JSDocs arguments to methods /** * @private */ class DTXTrianglesState { constructor() { /** * Texture that holds colors/pickColors/flags/flags2 per-object: * - columns: one concept per column => color / pick-color / ... * - row: the object Id * * @type BindableDataTexture */ this.texturePerObjectColorsAndFlags = null; /** * Texture that holds the XYZ offsets per-object: * - columns: just 1 column with the XYZ-offset * - row: the object Id * * @type BindableDataTexture */ this.texturePerObjectOffsets = null; this.texturePerObjectInstanceMatrices = null; /** * Texture that holds the objectDecodeAndInstanceMatrix per-object: * - columns: each column is one column of the matrix * - row: the object Id * * @type BindableDataTexture */ this.texturePerObjectPositionsDecodeMatrix = null; /** * Texture that holds all the `different-vertices` used by the layer. * * @type BindableDataTexture */ this.texturePerVertexIdCoordinates = null; /** * Texture that holds the PortionId that corresponds to a given polygon-id. * * Variant of the texture for 8-bit based polygon-ids. * * @type BindableDataTexture */ this.texturePerPolygonIdPortionIds8Bits = null; /** * Texture that holds the PortionId that corresponds to a given polygon-id. * * Variant of the texture for 16-bit based polygon-ids. * * @type BindableDataTexture */ this.texturePerPolygonIdPortionIds16Bits = null; /** * Texture that holds the PortionId that corresponds to a given polygon-id. * * Variant of the texture for 32-bit based polygon-ids. * * @type BindableDataTexture */ this.texturePerPolygonIdPortionIds32Bits = null; /** * Texture that holds the PortionId that corresponds to a given edge-id. * * Variant of the texture for 8-bit based polygon-ids. * * @type BindableDataTexture */ this.texturePerEdgeIdPortionIds8Bits = null; /** * Texture that holds the PortionId that corresponds to a given edge-id. * * Variant of the texture for 16-bit based polygon-ids. * * @type BindableDataTexture */ this.texturePerEdgeIdPortionIds16Bits = null; /** * Texture that holds the PortionId that corresponds to a given edge-id. * * Variant of the texture for 32-bit based polygon-ids. * * @type BindableDataTexture */ this.texturePerEdgeIdPortionIds32Bits = null; /** * Texture that holds the unique-vertex-indices for 8-bit based indices. * * @type BindableDataTexture */ this.texturePerPolygonIdIndices8Bits = null; /** * Texture that holds the unique-vertex-indices for 16-bit based indices. * * @type BindableDataTexture */ this.texturePerPolygonIdIndices16Bits = null; /** * Texture that holds the unique-vertex-indices for 32-bit based indices. * * @type BindableDataTexture */ this.texturePerPolygonIdIndices32Bits = null; /** * Texture that holds the unique-vertex-indices for 8-bit based edge indices. * * @type BindableDataTexture */ this.texturePerPolygonIdEdgeIndices8Bits = null; /** * Texture that holds the unique-vertex-indices for 16-bit based edge indices. * * @type BindableDataTexture */ this.texturePerPolygonIdEdgeIndices16Bits = null; /** * Texture that holds the unique-vertex-indices for 32-bit based edge indices. * * @type BindableDataTexture */ this.texturePerPolygonIdEdgeIndices32Bits = null; /** * Texture that holds the model matrices * - columns: each column in the texture is a model matrix column. * - row: each row is a different model matrix. * * @type BindableDataTexture */ this.textureModelMatrices = null; } finalize() { this.indicesPerBitnessTextures = { 8: this.texturePerPolygonIdIndices8Bits, 16: this.texturePerPolygonIdIndices16Bits, 32: this.texturePerPolygonIdIndices32Bits, }; this.indicesPortionIdsPerBitnessTextures = { 8: this.texturePerPolygonIdPortionIds8Bits, 16: this.texturePerPolygonIdPortionIds16Bits, 32: this.texturePerPolygonIdPortionIds32Bits, }; this.edgeIndicesPerBitnessTextures = { 8: this.texturePerPolygonIdEdgeIndices8Bits, 16: this.texturePerPolygonIdEdgeIndices16Bits, 32: this.texturePerPolygonIdEdgeIndices32Bits, }; this.edgeIndicesPortionIdsPerBitnessTextures = { 8: this.texturePerEdgeIdPortionIds8Bits, 16: this.texturePerEdgeIdPortionIds16Bits, 32: this.texturePerEdgeIdPortionIds32Bits, }; } /** * * @param {Program} glProgram * @param {string} objectDecodeMatricesShaderName * @param {string} vertexTextureShaderName * @param {string} objectAttributesTextureShaderName * @param {string} objectMatricesShaderName */ bindCommonTextures( glProgram, objectDecodeMatricesShaderName, vertexTextureShaderName, objectAttributesTextureShaderName, objectMatricesShaderName ) { this.texturePerObjectPositionsDecodeMatrix.bindTexture(glProgram, objectDecodeMatricesShaderName, 1); this.texturePerVertexIdCoordinates.bindTexture(glProgram, vertexTextureShaderName, 2); this.texturePerObjectColorsAndFlags.bindTexture(glProgram, objectAttributesTextureShaderName, 3); this.texturePerObjectInstanceMatrices.bindTexture(glProgram, objectMatricesShaderName, 4); } /** * * @param {Program} glProgram * @param {string} portionIdsShaderName * @param {string} polygonIndicesShaderName * @param {8|16|32} textureBitness */ bindTriangleIndicesTextures( glProgram, portionIdsShaderName, polygonIndicesShaderName, textureBitness, ) { this.indicesPortionIdsPerBitnessTextures[textureBitness].bindTexture( glProgram, portionIdsShaderName, 5 // webgl texture unit ); this.indicesPerBitnessTextures[textureBitness].bindTexture( glProgram, polygonIndicesShaderName, 6 // webgl texture unit ); } /** * * @param {Program} glProgram * @param {string} edgePortionIdsShaderName * @param {string} edgeIndicesShaderName * @param {8|16|32} textureBitness */ bindEdgeIndicesTextures( glProgram, edgePortionIdsShaderName, edgeIndicesShaderName, textureBitness, ) { this.edgeIndicesPortionIdsPerBitnessTextures[textureBitness].bindTexture( glProgram, edgePortionIdsShaderName, 5 // webgl texture unit ); this.edgeIndicesPerBitnessTextures[textureBitness].bindTexture( glProgram, edgeIndicesShaderName, 6 // webgl texture unit ); } } const dataTextureRamStats = { sizeDataColorsAndFlags: 0, sizeDataPositionDecodeMatrices: 0, sizeDataTextureOffsets: 0, sizeDataTexturePositions: 0, sizeDataTextureIndices: 0, sizeDataTextureEdgeIndices: 0, sizeDataTexturePortionIds: 0, numberOfGeometries: 0, numberOfPortions: 0, numberOfLayers: 0, numberOfTextures: 0, totalPolygons: 0, totalPolygons8Bits: 0, totalPolygons16Bits: 0, totalPolygons32Bits: 0, totalEdges: 0, totalEdges8Bits: 0, totalEdges16Bits: 0, totalEdges32Bits: 0, cannotCreatePortion: { because10BitsObjectId: 0, becauseTextureSize: 0, }, overheadSizeAlignementIndices: 0, overheadSizeAlignementEdgeIndices: 0, }; window.printDataTextureRamStats = function () { console.log(JSON.stringify(dataTextureRamStats, null, 4)); let totalRamSize = 0; Object.keys(dataTextureRamStats).forEach(key => { if (key.startsWith("size")) { totalRamSize += dataTextureRamStats[key]; } }); console.log(`Total size ${totalRamSize} bytes (${(totalRamSize / 1000 / 1000).toFixed(2)} MB)`); console.log(`Avg bytes / triangle: ${(totalRamSize / dataTextureRamStats.totalPolygons).toFixed(2)}`); let percentualRamStats = {}; Object.keys(dataTextureRamStats).forEach(key => { if (key.startsWith("size")) { percentualRamStats[key] = `${(dataTextureRamStats[key] / totalRamSize * 100).toFixed(2)} % of total`; } }); console.log(JSON.stringify({percentualRamUsage: percentualRamStats}, null, 4)); }; /** * @private */ class DTXTrianglesTextureFactory { constructor() { } /** * Enables the currently binded ````WebGLTexture```` to be used as a data texture. * * @param {WebGL2RenderingContext} gl * * @private */ disableBindedTextureFiltering(gl) { gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); } /** * This will generate an RGBA texture for: * - colors * - pickColors * - flags * - flags2 * - vertex bases * - vertex base offsets * * The texture will have: * - 4 RGBA columns per row: for each object (pick) color and flags(2) * - N rows where N is the number of objects * * @param {WebGL2RenderingContext} gl * @param {ArrayLike>} colors Array of colors for all objects in the layer * @param {ArrayLike>} pickColors Array of pickColors for all objects in the layer * @param {ArrayLike} vertexBases Array of position-index-bases foteh all objects in the layer * @param {ArrayLike} indexBaseOffsets For triangles: array of offests between the (gl_VertexID / 3) and the position where the indices start in the texture layer * @param {ArrayLike} edgeIndexBaseOffsets For edges: Array of offests between the (gl_VertexID / 2) and the position where the edge indices start in the texture layer * @param {ArrayLike} solid Array is-solid flag for all objects in the layer * * @returns {BindableDataTexture} */ createTextureForColorsAndFlags(gl, colors, pickColors, vertexBases, indexBaseOffsets, edgeIndexBaseOffsets, solid) { const numPortions = colors.length; // The number of rows in the texture is the number of // objects in the layer. this.numPortions = numPortions; const textureWidth = 512 * 8; const textureHeight = Math.ceil(numPortions / (textureWidth / 8)); if (textureHeight === 0) { throw "texture height===0"; } // 8 columns per texture row: // - col0: (RGBA) object color RGBA // - col1: (packed Uint32 as RGBA) object pick color // - col2: (packed 4 bytes as RGBA) object flags // - col3: (packed 4 bytes as RGBA) object flags2 // - col4: (packed Uint32 bytes as RGBA) vertex base // - col5: (packed Uint32 bytes as RGBA) index base offset // - col6: (packed Uint32 bytes as RGBA) edge index base offset // - col7: (packed 4 bytes as RGBA) is-solid flag for objects const texArray = new Uint8Array(4 * textureWidth * textureHeight); dataTextureRamStats.sizeDataColorsAndFlags += texArray.byteLength; dataTextureRamStats.numberOfTextures++; for (let i = 0; i < numPortions; i++) { // object color texArray.set(colors [i], i * 32 + 0); texArray.set(pickColors [i], i * 32 + 4); // object pick color texArray.set([0, 0, 0, 0], i * 32 + 8); // object flags texArray.set([0, 0, 0, 0], i * 32 + 12); // object flags2 // vertex base texArray.set([ (vertexBases[i] >> 24) & 255, (vertexBases[i] >> 16) & 255, (vertexBases[i] >> 8) & 255, (vertexBases[i]) & 255, ], i * 32 + 16 ); // triangles index base offset texArray.set( [ (indexBaseOffsets[i] >> 24) & 255, (indexBaseOffsets[i] >> 16) & 255, (indexBaseOffsets[i] >> 8) & 255, (indexBaseOffsets[i]) & 255, ], i * 32 + 20 ); // edge index base offset texArray.set( [ (edgeIndexBaseOffsets[i] >> 24) & 255, (edgeIndexBaseOffsets[i] >> 16) & 255, (edgeIndexBaseOffsets[i] >> 8) & 255, (edgeIndexBaseOffsets[i]) & 255, ], i * 32 + 24 ); // is-solid flag texArray.set([solid[i] ? 1 : 0, 0, 0, 0], i * 32 + 28); } const texture = gl.createTexture(); gl.bindTexture(gl.TEXTURE_2D, texture); gl.texStorage2D(gl.TEXTURE_2D, 1, gl.RGBA8UI, textureWidth, textureHeight); gl.texSubImage2D(gl.TEXTURE_2D, 0, 0, 0, textureWidth, textureHeight, gl.RGBA_INTEGER, gl.UNSIGNED_BYTE, texArray, 0); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); gl.bindTexture(gl.TEXTURE_2D, null); return new BindableDataTexture(gl, texture, textureWidth, textureHeight, texArray); } /** * This will generate a texture for all object offsets. * * @param {WebGL2RenderingContext} gl * @param {int[]} offsets Array of int[3], one XYZ offset array for each object * * @returns {BindableDataTexture} */ createTextureForObjectOffsets(gl, numOffsets) { const textureWidth = 512; const textureHeight = Math.ceil(numOffsets / textureWidth); if (textureHeight === 0) { throw "texture height===0"; } const texArray = new Float32Array(3 * textureWidth * textureHeight).fill(0); dataTextureRamStats.sizeDataTextureOffsets += texArray.byteLength; dataTextureRamStats.numberOfTextures++; const texture = gl.createTexture(); gl.bindTexture(gl.TEXTURE_2D, texture); gl.texStorage2D(gl.TEXTURE_2D, 1, gl.RGB32F, textureWidth, textureHeight); gl.texSubImage2D(gl.TEXTURE_2D, 0, 0, 0, textureWidth, textureHeight, gl.RGB, gl.FLOAT, texArray, 0); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); gl.bindTexture(gl.TEXTURE_2D, null); return new BindableDataTexture(gl, texture, textureWidth, textureHeight, texArray); } /** * This will generate a texture for all positions decode matrices in the layer. * * The texture will have: * - 4 RGBA columns per row (each column will contain 4 packed half-float (16 bits) components). * Thus, each row will contain 16 packed half-floats corresponding to a complete positions decode matrix) * - N rows where N is the number of objects * * @param {WebGL2RenderingContext} gl * @param {ArrayLike} instanceMatrices Array of geometry instancing matrices for all objects in the layer. Null if the objects are not instanced. * * @returns {BindableDataTexture} */ createTextureForInstancingMatrices(gl, instanceMatrices) { const numMatrices = instanceMatrices.length; if (numMatrices === 0) { throw "num instance matrices===0"; } // in one row we can fit 512 matrices const textureWidth = 512 * 4; const textureHeight = Math.ceil(numMatrices / (textureWidth / 4)); const texArray = new Float32Array(4 * textureWidth * textureHeight); // dataTextureRamStats.sizeDataPositionDecodeMatrices += texArray.byteLength; dataTextureRamStats.numberOfTextures++; for (let i = 0; i < instanceMatrices.length; i++) { // 4x4 values texArray.set(instanceMatrices[i], i * 16); } const texture = gl.createTexture(); gl.bindTexture(gl.TEXTURE_2D, texture); gl.texStorage2D(gl.TEXTURE_2D, 1, gl.RGBA32F, textureWidth, textureHeight); gl.texSubImage2D(gl.TEXTURE_2D, 0, 0, 0, textureWidth, textureHeight, gl.RGBA, gl.FLOAT, texArray, 0); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); gl.bindTexture(gl.TEXTURE_2D, null); return new BindableDataTexture(gl, texture, textureWidth, textureHeight, texArray); } /** * This will generate a texture for all positions decode matrices in the layer. * * The texture will have: * - 4 RGBA columns per row (each column will contain 4 packed half-float (16 bits) components). * Thus, each row will contain 16 packed half-floats corresponding to a complete positions decode matrix) * - N rows where N is the number of objects * * @param {WebGL2RenderingContext} gl * @param {ArrayLike} positionDecodeMatrices Array of positions decode matrices for all objects in the layer * * @returns {BindableDataTexture} */ createTextureForPositionsDecodeMatrices(gl, positionDecodeMatrices) { const numMatrices = positionDecodeMatrices.length; if (numMatrices === 0) { throw "num decode+entity matrices===0"; } // in one row we can fit 512 matrices const textureWidth = 512 * 4; const textureHeight = Math.ceil(numMatrices / (textureWidth / 4)); const texArray = new Float32Array(4 * textureWidth * textureHeight); dataTextureRamStats.sizeDataPositionDecodeMatrices += texArray.byteLength; dataTextureRamStats.numberOfTextures++; for (let i = 0; i < positionDecodeMatrices.length; i++) { // 4x4 values texArray.set(positionDecodeMatrices[i], i * 16); } const texture = gl.createTexture(); gl.bindTexture(gl.TEXTURE_2D, texture); gl.texStorage2D(gl.TEXTURE_2D, 1, gl.RGBA32F, textureWidth, textureHeight); gl.texSubImage2D(gl.TEXTURE_2D, 0, 0, 0, textureWidth, textureHeight, gl.RGBA, gl.FLOAT, texArray, 0); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); gl.bindTexture(gl.TEXTURE_2D, null); return new BindableDataTexture(gl, texture, textureWidth, textureHeight); } /** * @param {WebGL2RenderingContext} gl * @param indicesArrays * @param lenIndices * * @returns {BindableDataTexture} */ createTextureFor8BitIndices(gl, indicesArrays, lenIndices) { if (lenIndices === 0) { return {texture: null, textureHeight: 0,}; } const textureWidth = 4096; const textureHeight = Math.ceil(lenIndices / 3 / textureWidth); if (textureHeight === 0) { throw "texture height===0"; } const texArraySize = textureWidth * textureHeight * 3; const texArray = new Uint8Array(texArraySize); dataTextureRamStats.sizeDataTextureIndices += texArray.byteLength; dataTextureRamStats.numberOfTextures++; for (let i = 0, j = 0, len = indicesArrays.length; i < len; i++) { const pc = indicesArrays[i]; texArray.set(pc, j); j += pc.length; } const texture = gl.createTexture(); gl.bindTexture(gl.TEXTURE_2D, texture); gl.texStorage2D(gl.TEXTURE_2D, 1, gl.RGB8UI, textureWidth, textureHeight); gl.texSubImage2D(gl.TEXTURE_2D, 0, 0, 0, textureWidth, textureHeight, gl.RGB_INTEGER, gl.UNSIGNED_BYTE, texArray, 0); this.disableBindedTextureFiltering(gl); gl.bindTexture(gl.TEXTURE_2D, null); return new BindableDataTexture(gl, texture, textureWidth, textureHeight); } /** * @param {WebGL2RenderingContext} gl * @param indicesArrays * @param lenIndices * * @returns {BindableDataTexture} */ createTextureFor16BitIndices(gl, indicesArrays, lenIndices) { if (lenIndices === 0) { return {texture: null, textureHeight: 0,}; } const textureWidth = 4096; const textureHeight = Math.ceil(lenIndices / 3 / textureWidth); if (textureHeight === 0) { throw "texture height===0"; } const texArraySize = textureWidth * textureHeight * 3; const texArray = new Uint16Array(texArraySize); dataTextureRamStats.sizeDataTextureIndices += texArray.byteLength; dataTextureRamStats.numberOfTextures++; for (let i = 0, j = 0, len = indicesArrays.length; i < len; i++) { const pc = indicesArrays[i]; texArray.set(pc, j); j += pc.length; } const texture = gl.createTexture(); gl.bindTexture(gl.TEXTURE_2D, texture); gl.texStorage2D(gl.TEXTURE_2D, 1, gl.RGB16UI, textureWidth, textureHeight); gl.texSubImage2D(gl.TEXTURE_2D, 0, 0, 0, textureWidth, textureHeight, gl.RGB_INTEGER, gl.UNSIGNED_SHORT, texArray, 0); this.disableBindedTextureFiltering(gl); gl.bindTexture(gl.TEXTURE_2D, null); return new BindableDataTexture(gl, texture, textureWidth, textureHeight); } /** * @param {WebGL2RenderingContext} gl * @param indicesArrays * @param lenIndices * * @returns {BindableDataTexture} */ createTextureFor32BitIndices(gl, indicesArrays, lenIndices) { if (lenIndices === 0) { return {texture: null, textureHeight: 0,}; } const textureWidth = 4096; const textureHeight = Math.ceil(lenIndices / 3 / textureWidth); if (textureHeight === 0) { throw "texture height===0"; } const texArraySize = textureWidth * textureHeight * 3; const texArray = new Uint32Array(texArraySize); dataTextureRamStats.sizeDataTextureIndices += texArray.byteLength; dataTextureRamStats.numberOfTextures++; for (let i = 0, j = 0, len = indicesArrays.length; i < len; i++) { const pc = indicesArrays[i]; texArray.set(pc, j); j += pc.length; } const texture = gl.createTexture(); gl.bindTexture(gl.TEXTURE_2D, texture); gl.texStorage2D(gl.TEXTURE_2D, 1, gl.RGB32UI, textureWidth, textureHeight); gl.texSubImage2D(gl.TEXTURE_2D, 0, 0, 0, textureWidth, textureHeight, gl.RGB_INTEGER, gl.UNSIGNED_INT, texArray, 0); this.disableBindedTextureFiltering(gl); gl.bindTexture(gl.TEXTURE_2D, null); return new BindableDataTexture(gl, texture, textureWidth, textureHeight); } /** * @param {WebGL2RenderingContext} gl * @param indicesArrays * @param lenIndices * * @returns {BindableDataTexture} */ createTextureFor8BitsEdgeIndices(gl, indicesArrays, lenIndices) { if (lenIndices === 0) { return {texture: null, textureHeight: 0,}; } const textureWidth = 4096; const textureHeight = Math.ceil(lenIndices / 2 / textureWidth); if (textureHeight === 0) { throw "texture height===0"; } const texArraySize = textureWidth * textureHeight * 2; const texArray = new Uint8Array(texArraySize); dataTextureRamStats.sizeDataTextureEdgeIndices += texArray.byteLength; dataTextureRamStats.numberOfTextures++; for (let i = 0, j = 0, len = indicesArrays.length; i < len; i++) { const pc = indicesArrays[i]; texArray.set(pc, j); j += pc.length; } const texture = gl.createTexture(); gl.bindTexture(gl.TEXTURE_2D, texture); gl.texStorage2D(gl.TEXTURE_2D, 1, gl.RG8UI, textureWidth, textureHeight); gl.texSubImage2D(gl.TEXTURE_2D, 0, 0, 0, textureWidth, textureHeight, gl.RG_INTEGER, gl.UNSIGNED_BYTE, texArray, 0); this.disableBindedTextureFiltering(gl); gl.bindTexture(gl.TEXTURE_2D, null); return new BindableDataTexture(gl, texture, textureWidth, textureHeight); } /** * @param {WebGL2RenderingContext} gl * @param indicesArrays * @param lenIndices * * @returns {BindableDataTexture} */ createTextureFor16BitsEdgeIndices(gl, indicesArrays, lenIndices) { if (lenIndices === 0) { return {texture: null, textureHeight: 0,}; } const textureWidth = 4096; const textureHeight = Math.ceil(lenIndices / 2 / textureWidth); if (textureHeight === 0) { throw "texture height===0"; } const texArraySize = textureWidth * textureHeight * 2; const texArray = new Uint16Array(texArraySize); dataTextureRamStats.sizeDataTextureEdgeIndices += texArray.byteLength; dataTextureRamStats.numberOfTextures++; for (let i = 0, j = 0, len = indicesArrays.length; i < len; i++) { const pc = indicesArrays[i]; texArray.set(pc, j); j += pc.length; } const texture = gl.createTexture(); gl.bindTexture(gl.TEXTURE_2D, texture); gl.texStorage2D(gl.TEXTURE_2D, 1, gl.RG16UI, textureWidth, textureHeight); gl.texSubImage2D(gl.TEXTURE_2D, 0, 0, 0, textureWidth, textureHeight, gl.RG_INTEGER, gl.UNSIGNED_SHORT, texArray, 0); this.disableBindedTextureFiltering(gl); gl.bindTexture(gl.TEXTURE_2D, null); return new BindableDataTexture(gl, texture, textureWidth, textureHeight); } /** * @param {WebGL2RenderingContext} gl * @param indicesArrays * @param lenIndices * * @returns {BindableDataTexture} */ createTextureFor32BitsEdgeIndices(gl, indicesArrays, lenIndices) { if (lenIndices === 0) { return {texture: null, textureHeight: 0,}; } const textureWidth = 4096; const textureHeight = Math.ceil(lenIndices / 2 / textureWidth); if (textureHeight === 0) { throw "texture height===0"; } const texArraySize = textureWidth * textureHeight * 2; const texArray = new Uint32Array(texArraySize); dataTextureRamStats.sizeDataTextureEdgeIndices += texArray.byteLength; dataTextureRamStats.numberOfTextures++; for (let i = 0, j = 0, len = indicesArrays.length; i < len; i++) { const pc = indicesArrays[i]; texArray.set(pc, j); j += pc.length; } const texture = gl.createTexture(); gl.bindTexture(gl.TEXTURE_2D, texture); gl.texStorage2D(gl.TEXTURE_2D, 1, gl.RG32UI, textureWidth, textureHeight); gl.texSubImage2D(gl.TEXTURE_2D, 0, 0, 0, textureWidth, textureHeight, gl.RG_INTEGER, gl.UNSIGNED_INT, texArray, 0); this.disableBindedTextureFiltering(gl); gl.bindTexture(gl.TEXTURE_2D, null); return new BindableDataTexture(gl, texture, textureWidth, textureHeight); } /** * @param {WebGL2RenderingContext} gl * @param {ArrayLike} positionsArrays Arrays of quantized positions in the layer * @param lenPositions * * This will generate a texture for positions in the layer. * * The texture will have: * - 1024 columns, where each pixel will be a 16-bit-per-component RGB texture, corresponding to the XYZ of the position * - a number of rows R where R*1024 is just >= than the number of vertices (positions / 3) * * @returns {BindableDataTexture} */ createTextureForPositions(gl, positionsArrays, lenPositions) { const numVertices = lenPositions / 3; const textureWidth = 4096; const textureHeight = Math.ceil(numVertices / textureWidth); if (textureHeight === 0) { throw "texture height===0"; } const texArraySize = textureWidth * textureHeight * 3; const texArray = new Uint16Array(texArraySize); dataTextureRamStats.sizeDataTexturePositions += texArray.byteLength; dataTextureRamStats.numberOfTextures++; for (let i = 0, j = 0, len = positionsArrays.length; i < len; i++) { const pc = positionsArrays[i]; texArray.set(pc, j); j += pc.length; } const texture = gl.createTexture(); gl.bindTexture(gl.TEXTURE_2D, texture); gl.texStorage2D(gl.TEXTURE_2D, 1, gl.RGB16UI, textureWidth, textureHeight); gl.texSubImage2D(gl.TEXTURE_2D, 0, 0, 0, textureWidth, textureHeight, gl.RGB_INTEGER, gl.UNSIGNED_SHORT, texArray, 0); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); gl.bindTexture(gl.TEXTURE_2D, null); return new BindableDataTexture(gl, texture, textureWidth, textureHeight); } /** * @param {WebGL2RenderingContext} gl * @param {ArrayLike} portionIdsArray * * @returns {BindableDataTexture} */ createTextureForPackedPortionIds(gl, portionIdsArray) { if (portionIdsArray.length === 0) { return {texture: null, textureHeight: 0,}; } const lenArray = portionIdsArray.length; const textureWidth = 4096; const textureHeight = Math.ceil(lenArray / textureWidth); if (textureHeight === 0) { throw "texture height===0"; } const texArraySize = textureWidth * textureHeight; const texArray = new Uint16Array(texArraySize); texArray.set(portionIdsArray, 0); dataTextureRamStats.sizeDataTexturePortionIds += texArray.byteLength; dataTextureRamStats.numberOfTextures++; const texture = gl.createTexture(); gl.bindTexture(gl.TEXTURE_2D, texture); gl.texStorage2D(gl.TEXTURE_2D, 1, gl.R16UI, textureWidth, textureHeight); gl.texSubImage2D(gl.TEXTURE_2D, 0, 0, 0, textureWidth, textureHeight, gl.RED_INTEGER, gl.UNSIGNED_SHORT, texArray, 0); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); gl.bindTexture(gl.TEXTURE_2D, null); return new BindableDataTexture(gl, texture, textureWidth, textureHeight); } } const configs = new Configs(); /** * 12-bits allowed for object ids. * Limits the per-object texture height in the layer. */ const MAX_NUMBER_OF_OBJECTS_IN_LAYER = (1 << 16); /** * 4096 is max data texture height. * Limits the aggregated geometry texture height in the layer. */ const MAX_DATA_TEXTURE_HEIGHT = configs.maxDataTextureHeight; /** * Align `indices` and `edgeIndices` memory layout to 8 elements. * * Used as an optimization for the `...portionIds...` texture, so it * can just be stored 1 out of 8 `portionIds` corresponding to a given * `triangle-index` or `edge-index`. */ const INDICES_EDGE_INDICES_ALIGNEMENT_SIZE = 8; /** * Number of maximum allowed per-object flags update per render frame * before switching to batch update mode. */ const MAX_OBJECT_UPDATES_IN_FRAME_WITHOUT_BATCHED_UPDATE = 10; const tempVec4a$a = math.vec4(); const tempMat4a = new Float32Array(16); const tempUint8Array4 = new Uint8Array(4); const tempFloat32Array3 = new Float32Array(3); let numLayers = 0; const DEFAULT_MATRIX$1 = math.identityMat4(); /** * @private */ class DTXTrianglesLayer { constructor(model, cfg) { //console.info("Creating DTXTrianglesLayer"); dataTextureRamStats.numberOfLayers++; this._layerNumber = numLayers++; this.sortId = `TriDTX-${this._layerNumber}`; // State sorting key. this.layerIndex = cfg.layerIndex; // Index of this TrianglesDataTextureLayer in {@link SceneModel#_layerList}. this._renderers = getRenderers(model.scene); this.model = model; this._buffer = new DTXTrianglesBuffer(); this._dtxState = new DTXTrianglesState(); this._dtxTextureFactory = new DTXTrianglesTextureFactory(); this._state = new RenderState({ origin: math.vec3(cfg.origin), metallicRoughnessBuf: null, textureState: this._dtxState, numIndices8Bits: 0, numIndices16Bits: 0, numIndices32Bits: 0, numEdgeIndices8Bits: 0, numEdgeIndices16Bits: 0, numEdgeIndices32Bits: 0, numVertices: 0, }); this._numPortions = 0; // These counts are used to avoid unnecessary render passes this._numVisibleLayerPortions = 0; this._numTransparentLayerPortions = 0; this._numXRayedLayerPortions = 0; this._numSelectedLayerPortions = 0; this._numHighlightedLayerPortions = 0; this._numClippableLayerPortions = 0; this._numEdgesLayerPortions = 0; this._numPickableLayerPortions = 0; this._numCulledLayerPortions = 0; this._subPortions = []; if (this.model.scene.readableGeometryEnabled) { this._subPortionReadableGeometries = {}; } /** * Due to `index rebucketting` process in ```prepareMeshGeometry``` function, it's possible that a single * portion is expanded to more than 1 real sub-portion. * * This Array tracks the mapping between: * * - external `portionIds` as seen by consumers of this class. * - internal `sub-portionIds` actually managed by this class. * * The outer index of this array is the externally seen `portionId`. * The inner value of the array, are `sub-portionIds` corresponding to the `portionId`. */ this._portionToSubPortionsMap = []; this._bucketGeometries = {}; this._meshes = []; /** * The axis-aligned World-space boundary of this TrianglesDataTextureLayer's positions. */ this._aabb = math.collapseAABB3(); this.aabbDirty = true; /** * The number of updates in the current frame; */ this._numUpdatesInFrame = 0; /** * The type of primitives in this layer. */ this.primitive = cfg.primitive; this._finalized = false; } get aabb() { if (this.aabbDirty) { math.collapseAABB3(this._aabb); for (let i = 0, len = this._meshes.length; i < len; i++) { math.expandAABB3(this._aabb, this._meshes[i].aabb); } this.aabbDirty = false; } return this._aabb; } /** * Returns whether the ```TrianglesDataTextureLayer``` has room for more portions. * * @param {object} portionCfg An object containing the geometrical data (`positions`, `indices`, `edgeIndices`) for the portion. * @returns {Boolean} Wheter the requested portion can be created */ canCreatePortion(portionCfg) { if (this._finalized) { throw "Already finalized"; } const numNewPortions = portionCfg.buckets.length; if ((this._numPortions + numNewPortions) > MAX_NUMBER_OF_OBJECTS_IN_LAYER) { dataTextureRamStats.cannotCreatePortion.because10BitsObjectId++; } let retVal = (this._numPortions + numNewPortions) <= MAX_NUMBER_OF_OBJECTS_IN_LAYER; const bucketIndex = 0; // TODO: Is this a bug? const bucketGeometryId = portionCfg.geometryId !== undefined && portionCfg.geometryId !== null ? `${portionCfg.geometryId}#${bucketIndex}` : `${portionCfg.id}#${bucketIndex}`; const alreadyHasPortionGeometry = this._bucketGeometries[bucketGeometryId]; if (!alreadyHasPortionGeometry) { const maxIndicesOfAnyBits = Math.max(this._state.numIndices8Bits, this._state.numIndices16Bits, this._state.numIndices32Bits,); let numVertices = 0; let numIndices = 0; portionCfg.buckets.forEach(bucket => { numVertices += bucket.positionsCompressed.length / 3; numIndices += bucket.indices.length / 3; }); if ((this._state.numVertices + numVertices) > MAX_DATA_TEXTURE_HEIGHT * 4096 || (maxIndicesOfAnyBits + numIndices) > MAX_DATA_TEXTURE_HEIGHT * 4096) { dataTextureRamStats.cannotCreatePortion.becauseTextureSize++; } retVal &&= (this._state.numVertices + numVertices) <= MAX_DATA_TEXTURE_HEIGHT * 4096 && (maxIndicesOfAnyBits + numIndices) <= MAX_DATA_TEXTURE_HEIGHT * 4096; } return retVal; } /** * Creates a new portion within this TrianglesDataTextureLayer, returns the new portion ID. * * Gives the portion the specified geometry, color and matrix. * * @param mesh The SceneModelMesh that owns the portion * @param portionCfg.positionsCompressed Flat float Local-space positionsCompressed array. * @param [portionCfg.normals] Flat float normals array. * @param [portionCfg.colors] Flat float colors array. * @param portionCfg.indices Flat int indices array. * @param [portionCfg.edgeIndices] Flat int edges indices array. * @param portionCfg.color Quantized RGB color [0..255,0..255,0..255,0..255] * @param portionCfg.metallic Metalness factor [0..255] * @param portionCfg.roughness Roughness factor [0..255] * @param portionCfg.opacity Opacity [0..255] * @param [portionCfg.meshMatrix] Flat float 4x4 matrix - transforms the portion within the coordinate system that's local to the SceneModel * @param portionCfg.worldAABB Flat float AABB World-space AABB * @param portionCfg.pickColor Quantized pick color * @returns {number} Portion ID */ createPortion(mesh, portionCfg) { if (this._finalized) { throw "Already finalized"; } const subPortionIds = []; // const portionAABB = portionCfg.worldAABB; portionCfg.buckets.forEach((bucket, bucketIndex) => { const bucketGeometryId = portionCfg.geometryId !== undefined && portionCfg.geometryId !== null ? `${portionCfg.geometryId}#${bucketIndex}` : `${portionCfg.id}#${bucketIndex}`; let bucketGeometry = this._bucketGeometries[bucketGeometryId]; if (!bucketGeometry) { bucketGeometry = this._createBucketGeometry(portionCfg, bucket); this._bucketGeometries[bucketGeometryId] = bucketGeometry; } // const subPortionAABB = math.collapseAABB3(tempAABB3b); const subPortionId = this._createSubPortion(portionCfg, bucketGeometry, bucket); //math.expandAABB3(portionAABB, subPortionAABB); subPortionIds.push(subPortionId); }); const portionId = this._portionToSubPortionsMap.length; this._portionToSubPortionsMap.push(subPortionIds); this.model.numPortions++; this._meshes.push(mesh); return portionId; } _createBucketGeometry(portionCfg, bucket) { // Indices alignement // This will make every mesh consume a multiple of INDICES_EDGE_INDICES_ALIGNEMENT_SIZE // array items for storing the triangles of the mesh, and it supports: // - a memory optimization of factor INDICES_EDGE_INDICES_ALIGNEMENT_SIZE // - in exchange for a small RAM overhead // (by adding some padding until a size that is multiple of INDICES_EDGE_INDICES_ALIGNEMENT_SIZE) if (bucket.indices) { const alignedIndicesLen = Math.ceil((bucket.indices.length / 3) / INDICES_EDGE_INDICES_ALIGNEMENT_SIZE) * INDICES_EDGE_INDICES_ALIGNEMENT_SIZE * 3; dataTextureRamStats.overheadSizeAlignementIndices += 2 * (alignedIndicesLen - bucket.indices.length); const alignedIndices = new Uint32Array(alignedIndicesLen); alignedIndices.fill(0); alignedIndices.set(bucket.indices); bucket.indices = alignedIndices; } // EdgeIndices alignement // This will make every mesh consume a multiple of INDICES_EDGE_INDICES_ALIGNEMENT_SIZE // array items for storing the edges of the mesh, and it supports: // - a memory optimization of factor INDICES_EDGE_INDICES_ALIGNEMENT_SIZE // - in exchange for a small RAM overhead // (by adding some padding until a size that is multiple of INDICES_EDGE_INDICES_ALIGNEMENT_SIZE) if (bucket.edgeIndices) { const alignedEdgeIndicesLen = Math.ceil((bucket.edgeIndices.length / 2) / INDICES_EDGE_INDICES_ALIGNEMENT_SIZE) * INDICES_EDGE_INDICES_ALIGNEMENT_SIZE * 2; dataTextureRamStats.overheadSizeAlignementEdgeIndices += 2 * (alignedEdgeIndicesLen - bucket.edgeIndices.length); const alignedEdgeIndices = new Uint32Array(alignedEdgeIndicesLen); alignedEdgeIndices.fill(0); alignedEdgeIndices.set(bucket.edgeIndices); bucket.edgeIndices = alignedEdgeIndices; } const positionsCompressed = bucket.positionsCompressed; const indices = bucket.indices; const edgeIndices = bucket.edgeIndices; const buffer = this._buffer; buffer.positionsCompressed.push(positionsCompressed); const vertexBase = buffer.lenPositionsCompressed / 3; const numVertices = positionsCompressed.length / 3; buffer.lenPositionsCompressed += positionsCompressed.length; let indicesBase; let numTriangles = 0; if (indices) { numTriangles = indices.length / 3; let indicesBuffer; if (numVertices <= (1 << 8)) { indicesBuffer = buffer.indices8Bits; indicesBase = buffer.lenIndices8Bits / 3; buffer.lenIndices8Bits += indices.length; } else if (numVertices <= (1 << 16)) { indicesBuffer = buffer.indices16Bits; indicesBase = buffer.lenIndices16Bits / 3; buffer.lenIndices16Bits += indices.length; } else { indicesBuffer = buffer.indices32Bits; indicesBase = buffer.lenIndices32Bits / 3; buffer.lenIndices32Bits += indices.length; } indicesBuffer.push(indices); } let edgeIndicesBase; let numEdges = 0; if (edgeIndices) { numEdges = edgeIndices.length / 2; let edgeIndicesBuffer; if (numVertices <= (1 << 8)) { edgeIndicesBuffer = buffer.edgeIndices8Bits; edgeIndicesBase = buffer.lenEdgeIndices8Bits / 2; buffer.lenEdgeIndices8Bits += edgeIndices.length; } else if (numVertices <= (1 << 16)) { edgeIndicesBuffer = buffer.edgeIndices16Bits; edgeIndicesBase = buffer.lenEdgeIndices16Bits / 2; buffer.lenEdgeIndices16Bits += edgeIndices.length; } else { edgeIndicesBuffer = buffer.edgeIndices32Bits; edgeIndicesBase = buffer.lenEdgeIndices32Bits / 2; buffer.lenEdgeIndices32Bits += edgeIndices.length; } edgeIndicesBuffer.push(edgeIndices); } this._state.numVertices += numVertices; dataTextureRamStats.numberOfGeometries++; const bucketGeometry = { vertexBase, numVertices, numTriangles, numEdges, indicesBase, edgeIndicesBase }; return bucketGeometry; } _createSubPortion(portionCfg, bucketGeometry, bucket, subPortionAABB) { const color = portionCfg.color; portionCfg.metallic; portionCfg.roughness; const colors = portionCfg.colors; const opacity = portionCfg.opacity; const meshMatrix = portionCfg.meshMatrix; const pickColor = portionCfg.pickColor; const buffer = this._buffer; const state = this._state; buffer.perObjectPositionsDecodeMatrices.push(portionCfg.positionsDecodeMatrix); buffer.perObjectInstancePositioningMatrices.push(meshMatrix || DEFAULT_MATRIX$1); buffer.perObjectSolid.push(!!portionCfg.solid); if (colors) { buffer.perObjectColors.push([colors[0] * 255, colors[1] * 255, colors[2] * 255, 255]); } else if (color) { // Color is pre-quantized by SceneModel buffer.perObjectColors.push([color[0], color[1], color[2], opacity]); } buffer.perObjectPickColors.push(pickColor); buffer.perObjectVertexBases.push(bucketGeometry.vertexBase); { let currentNumIndices; if (bucketGeometry.numVertices <= (1 << 8)) { currentNumIndices = state.numIndices8Bits; } else if (bucketGeometry.numVertices <= (1 << 16)) { currentNumIndices = state.numIndices16Bits; } else { currentNumIndices = state.numIndices32Bits; } buffer.perObjectIndexBaseOffsets.push(currentNumIndices / 3 - bucketGeometry.indicesBase); } { let currentNumEdgeIndices; if (bucketGeometry.numVertices <= (1 << 8)) { currentNumEdgeIndices = state.numEdgeIndices8Bits; } else if (bucketGeometry.numVertices <= (1 << 16)) { currentNumEdgeIndices = state.numEdgeIndices16Bits; } else { currentNumEdgeIndices = state.numEdgeIndices32Bits; } buffer.perObjectEdgeIndexBaseOffsets.push(currentNumEdgeIndices / 2 - bucketGeometry.edgeIndicesBase); } const subPortionId = this._subPortions.length; if (bucketGeometry.numTriangles > 0) { let numIndices = bucketGeometry.numTriangles * 3; let indicesPortionIdBuffer; if (bucketGeometry.numVertices <= (1 << 8)) { indicesPortionIdBuffer = buffer.perTriangleNumberPortionId8Bits; state.numIndices8Bits += numIndices; dataTextureRamStats.totalPolygons8Bits += bucketGeometry.numTriangles; } else if (bucketGeometry.numVertices <= (1 << 16)) { indicesPortionIdBuffer = buffer.perTriangleNumberPortionId16Bits; state.numIndices16Bits += numIndices; dataTextureRamStats.totalPolygons16Bits += bucketGeometry.numTriangles; } else { indicesPortionIdBuffer = buffer.perTriangleNumberPortionId32Bits; state.numIndices32Bits += numIndices; dataTextureRamStats.totalPolygons32Bits += bucketGeometry.numTriangles; } dataTextureRamStats.totalPolygons += bucketGeometry.numTriangles; for (let i = 0; i < bucketGeometry.numTriangles; i += INDICES_EDGE_INDICES_ALIGNEMENT_SIZE) { indicesPortionIdBuffer.push(subPortionId); } } if (bucketGeometry.numEdges > 0) { let numEdgeIndices = bucketGeometry.numEdges * 2; let edgeIndicesPortionIdBuffer; if (bucketGeometry.numVertices <= (1 << 8)) { edgeIndicesPortionIdBuffer = buffer.perEdgeNumberPortionId8Bits; state.numEdgeIndices8Bits += numEdgeIndices; dataTextureRamStats.totalEdges8Bits += bucketGeometry.numEdges; } else if (bucketGeometry.numVertices <= (1 << 16)) { edgeIndicesPortionIdBuffer = buffer.perEdgeNumberPortionId16Bits; state.numEdgeIndices16Bits += numEdgeIndices; dataTextureRamStats.totalEdges16Bits += bucketGeometry.numEdges; } else { edgeIndicesPortionIdBuffer = buffer.perEdgeNumberPortionId32Bits; state.numEdgeIndices32Bits += numEdgeIndices; dataTextureRamStats.totalEdges32Bits += bucketGeometry.numEdges; } dataTextureRamStats.totalEdges += bucketGeometry.numEdges; for (let i = 0; i < bucketGeometry.numEdges; i += INDICES_EDGE_INDICES_ALIGNEMENT_SIZE) { edgeIndicesPortionIdBuffer.push(subPortionId); } } // buffer.perObjectOffsets.push([0, 0, 0]); this._subPortions.push({ // vertsBase: vertsIndex, numVertices: bucketGeometry.numTriangles }); if (this.model.scene.readableGeometryEnabled) { this._subPortionReadableGeometries[subPortionId] = { indices: bucket.indices, positionsCompressed: bucket.positionsCompressed, positionsDecodeMatrix: portionCfg.positionsDecodeMatrix, meshMatrix: portionCfg.meshMatrix }; } this._numPortions++; dataTextureRamStats.numberOfPortions++; return subPortionId; } /** * Builds data textures from the appended geometries and loads them into the GPU. * * No more portions can then be created. */ finalize() { if (this._finalized) { return; } const state = this._state; const textureState = this._dtxState; const gl = this.model.scene.canvas.gl; const buffer = this._buffer; state.gl = gl; textureState.texturePerObjectColorsAndFlags = this._dtxTextureFactory.createTextureForColorsAndFlags( gl, buffer.perObjectColors, buffer.perObjectPickColors, buffer.perObjectVertexBases, buffer.perObjectIndexBaseOffsets, buffer.perObjectEdgeIndexBaseOffsets, buffer.perObjectSolid); textureState.texturePerObjectInstanceMatrices = this._dtxTextureFactory.createTextureForInstancingMatrices(gl, buffer.perObjectInstancePositioningMatrices); textureState.texturePerObjectPositionsDecodeMatrix = this._dtxTextureFactory.createTextureForPositionsDecodeMatrices( gl, buffer.perObjectPositionsDecodeMatrices); textureState.texturePerVertexIdCoordinates = this._dtxTextureFactory.createTextureForPositions( gl, buffer.positionsCompressed, buffer.lenPositionsCompressed); textureState.texturePerPolygonIdPortionIds8Bits = this._dtxTextureFactory.createTextureForPackedPortionIds( gl, buffer.perTriangleNumberPortionId8Bits); textureState.texturePerPolygonIdPortionIds16Bits = this._dtxTextureFactory.createTextureForPackedPortionIds( gl, buffer.perTriangleNumberPortionId16Bits); textureState.texturePerPolygonIdPortionIds32Bits = this._dtxTextureFactory.createTextureForPackedPortionIds( gl, buffer.perTriangleNumberPortionId32Bits); if (buffer.perEdgeNumberPortionId8Bits.length > 0) { textureState.texturePerEdgeIdPortionIds8Bits = this._dtxTextureFactory.createTextureForPackedPortionIds( gl, buffer.perEdgeNumberPortionId8Bits); } if (buffer.perEdgeNumberPortionId16Bits.length > 0) { textureState.texturePerEdgeIdPortionIds16Bits = this._dtxTextureFactory.createTextureForPackedPortionIds( gl, buffer.perEdgeNumberPortionId16Bits); } if (buffer.perEdgeNumberPortionId32Bits.length > 0) { textureState.texturePerEdgeIdPortionIds32Bits = this._dtxTextureFactory.createTextureForPackedPortionIds( gl, buffer.perEdgeNumberPortionId32Bits); } if (buffer.lenIndices8Bits > 0) { textureState.texturePerPolygonIdIndices8Bits = this._dtxTextureFactory.createTextureFor8BitIndices( gl, buffer.indices8Bits, buffer.lenIndices8Bits); } if (buffer.lenIndices16Bits > 0) { textureState.texturePerPolygonIdIndices16Bits = this._dtxTextureFactory.createTextureFor16BitIndices( gl, buffer.indices16Bits, buffer.lenIndices16Bits); } if (buffer.lenIndices32Bits > 0) { textureState.texturePerPolygonIdIndices32Bits = this._dtxTextureFactory.createTextureFor32BitIndices( gl, buffer.indices32Bits, buffer.lenIndices32Bits); } if (buffer.lenEdgeIndices8Bits > 0) { textureState.texturePerPolygonIdEdgeIndices8Bits = this._dtxTextureFactory.createTextureFor8BitsEdgeIndices( gl, buffer.edgeIndices8Bits, buffer.lenEdgeIndices8Bits); } if (buffer.lenEdgeIndices16Bits > 0) { textureState.texturePerPolygonIdEdgeIndices16Bits = this._dtxTextureFactory.createTextureFor16BitsEdgeIndices( gl, buffer.edgeIndices16Bits, buffer.lenEdgeIndices16Bits); } if (buffer.lenEdgeIndices32Bits > 0) { textureState.texturePerPolygonIdEdgeIndices32Bits = this._dtxTextureFactory.createTextureFor32BitsEdgeIndices( gl, buffer.edgeIndices32Bits, buffer.lenEdgeIndices32Bits); } textureState.finalize(); // Free up memory this._buffer = null; this._bucketGeometries = {}; this._finalized = true; this._deferredSetFlagsDirty = false; // this._onSceneRendering = this.model.scene.on("rendering", () => { if (this._deferredSetFlagsDirty) { this._uploadDeferredFlags(); } this._numUpdatesInFrame = 0; }); } isEmpty() { return this._numPortions === 0; } initFlags(portionId, flags, meshTransparent) { if (flags & ENTITY_FLAGS.VISIBLE) { this._numVisibleLayerPortions++; this.model.numVisibleLayerPortions++; } if (flags & ENTITY_FLAGS.HIGHLIGHTED) { this._numHighlightedLayerPortions++; this.model.numHighlightedLayerPortions++; } if (flags & ENTITY_FLAGS.XRAYED) { this._numXRayedLayerPortions++; this.model.numXRayedLayerPortions++; } if (flags & ENTITY_FLAGS.SELECTED) { this._numSelectedLayerPortions++; this.model.numSelectedLayerPortions++; } if (flags & ENTITY_FLAGS.CLIPPABLE) { this._numClippableLayerPortions++; this.model.numClippableLayerPortions++; } if (flags & ENTITY_FLAGS.EDGES) { this._numEdgesLayerPortions++; this.model.numEdgesLayerPortions++; } if (flags & ENTITY_FLAGS.PICKABLE) { this._numPickableLayerPortions++; this.model.numPickableLayerPortions++; } if (flags & ENTITY_FLAGS.CULLED) { this._numCulledLayerPortions++; this.model.numCulledLayerPortions++; } if (meshTransparent) { this._numTransparentLayerPortions++; this.model.numTransparentLayerPortions++; } const deferred = true; this._setFlags(portionId, flags, meshTransparent, deferred); this._setFlags2(portionId, flags, deferred); } flushInitFlags() { this._setDeferredFlags(); this._setDeferredFlags2(); } setVisible(portionId, flags, transparent) { if (!this._finalized) { throw "Not finalized"; } if (flags & ENTITY_FLAGS.VISIBLE) { this._numVisibleLayerPortions++; this.model.numVisibleLayerPortions++; } else { this._numVisibleLayerPortions--; this.model.numVisibleLayerPortions--; } this._setFlags(portionId, flags, transparent); } setHighlighted(portionId, flags, transparent) { if (!this._finalized) { throw "Not finalized"; } if (flags & ENTITY_FLAGS.HIGHLIGHTED) { this._numHighlightedLayerPortions++; this.model.numHighlightedLayerPortions++; } else { this._numHighlightedLayerPortions--; this.model.numHighlightedLayerPortions--; } this._setFlags(portionId, flags, transparent); } setXRayed(portionId, flags, transparent) { if (!this._finalized) { throw "Not finalized"; } if (flags & ENTITY_FLAGS.XRAYED) { this._numXRayedLayerPortions++; this.model.numXRayedLayerPortions++; } else { this._numXRayedLayerPortions--; this.model.numXRayedLayerPortions--; } this._setFlags(portionId, flags, transparent); } setSelected(portionId, flags, transparent) { if (!this._finalized) { throw "Not finalized"; } if (flags & ENTITY_FLAGS.SELECTED) { this._numSelectedLayerPortions++; this.model.numSelectedLayerPortions++; } else { this._numSelectedLayerPortions--; this.model.numSelectedLayerPortions--; } this._setFlags(portionId, flags, transparent); } setEdges(portionId, flags, transparent) { if (!this._finalized) { throw "Not finalized"; } if (flags & ENTITY_FLAGS.EDGES) { this._numEdgesLayerPortions++; this.model.numEdgesLayerPortions++; } else { this._numEdgesLayerPortions--; this.model.numEdgesLayerPortions--; } this._setFlags(portionId, flags, transparent); } setClippable(portionId, flags) { if (!this._finalized) { throw "Not finalized"; } if (flags & ENTITY_FLAGS.CLIPPABLE) { this._numClippableLayerPortions++; this.model.numClippableLayerPortions++; } else { this._numClippableLayerPortions--; this.model.numClippableLayerPortions--; } this._setFlags2(portionId, flags); } /** * This will _start_ a "set-flags transaction". * * After invoking this method, calling setFlags/setFlags2 will not update * the colors+flags texture but only store the new flags/flag2 in the * colors+flags texture data array. * * After invoking this method, and when all desired setFlags/setFlags2 have * been called on needed portions of the layer, invoke `_uploadDeferredFlags` * to actually upload the data array into the texture. * * In massive "set-flags" scenarios like VFC or LOD mechanisms, the combination of * `_beginDeferredFlags` + `_uploadDeferredFlags`brings a speed-up of * up to 80x when e.g. objects are massively (un)culled 🚀. */ _beginDeferredFlags() { this._deferredSetFlagsActive = true; } /** * This will _commit_ a "set-flags transaction". * * Invoking this method will update the colors+flags texture data with new * flags/flags2 set since the previous invocation of `_beginDeferredFlags`. */ _uploadDeferredFlags() { this._deferredSetFlagsActive = false; if (!this._deferredSetFlagsDirty) { return; } this._deferredSetFlagsDirty = false; const gl = this.model.scene.canvas.gl; const textureState = this._dtxState; gl.bindTexture(gl.TEXTURE_2D, textureState.texturePerObjectColorsAndFlags._texture); gl.texSubImage2D( gl.TEXTURE_2D, 0, // level 0, // xoffset 0, // yoffset textureState.texturePerObjectColorsAndFlags._textureWidth, // width textureState.texturePerObjectColorsAndFlags._textureHeight, // width gl.RGBA_INTEGER, gl.UNSIGNED_BYTE, textureState.texturePerObjectColorsAndFlags._textureData ); // gl.bindTexture(gl.TEXTURE_2D, textureState.texturePerObjectInstanceMatrices._texture); // gl.texSubImage2D( // gl.TEXTURE_2D, // 0, // level // 0, // xoffset // 0, // yoffset // textureState.texturePerObjectInstanceMatrices._textureWidth, // width // textureState.texturePerObjectInstanceMatrices._textureHeight, // width // gl.RGB, // gl.FLOAT, // textureState.texturePerObjectInstanceMatrices._textureData // ); } setCulled(portionId, flags, transparent) { if (!this._finalized) { throw "Not finalized"; } if (flags & ENTITY_FLAGS.CULLED) { this._numCulledLayerPortions += this._portionToSubPortionsMap[portionId].length; this.model.numCulledLayerPortions++; } else { this._numCulledLayerPortions -= this._portionToSubPortionsMap[portionId].length; this.model.numCulledLayerPortions--; } this._setFlags(portionId, flags, transparent); } setCollidable(portionId, flags) { if (!this._finalized) { throw "Not finalized"; } } setPickable(portionId, flags, transparent) { if (!this._finalized) { throw "Not finalized"; } if (flags & ENTITY_FLAGS.PICKABLE) { this._numPickableLayerPortions++; this.model.numPickableLayerPortions++; } else { this._numPickableLayerPortions--; this.model.numPickableLayerPortions--; } this._setFlags(portionId, flags, transparent); } setColor(portionId, color) { const subPortionIds = this._portionToSubPortionsMap[portionId]; for (let i = 0, len = subPortionIds.length; i < len; i++) { this._subPortionSetColor(subPortionIds[i], color); } } _subPortionSetColor(subPortionId, color) { if (!this._finalized) { throw "Not finalized"; } // Color const textureState = this._dtxState; const gl = this.model.scene.canvas.gl; tempUint8Array4 [0] = color[0]; tempUint8Array4 [1] = color[1]; tempUint8Array4 [2] = color[2]; tempUint8Array4 [3] = color[3]; // object colors textureState.texturePerObjectColorsAndFlags._textureData.set(tempUint8Array4, subPortionId * 32); if (this._deferredSetFlagsActive) { //console.info("_subPortionSetColor defer"); this._deferredSetFlagsDirty = true; return; } if (++this._numUpdatesInFrame >= MAX_OBJECT_UPDATES_IN_FRAME_WITHOUT_BATCHED_UPDATE) { this._beginDeferredFlags(); // Subsequent flags updates now deferred } //console.info("_subPortionSetColor write through"); gl.bindTexture(gl.TEXTURE_2D, textureState.texturePerObjectColorsAndFlags._texture); gl.texSubImage2D( gl.TEXTURE_2D, 0, // level (subPortionId % 512) * 8, // xoffset Math.floor(subPortionId / 512), // yoffset 1, // width 1, //height gl.RGBA_INTEGER, gl.UNSIGNED_BYTE, tempUint8Array4 ); // gl.bindTexture (gl.TEXTURE_2D, null); } setTransparent(portionId, flags, transparent) { if (transparent) { this._numTransparentLayerPortions++; this.model.numTransparentLayerPortions++; } else { this._numTransparentLayerPortions--; this.model.numTransparentLayerPortions--; } this._setFlags(portionId, flags, transparent); } _setFlags(portionId, flags, transparent, deferred = false) { const subPortionIds = this._portionToSubPortionsMap[portionId]; for (let i = 0, len = subPortionIds.length; i < len; i++) { this._subPortionSetFlags(subPortionIds[i], flags, transparent, deferred); } } _subPortionSetFlags(subPortionId, flags, transparent, deferred = false) { if (!this._finalized) { throw "Not finalized"; } const visible = !!(flags & ENTITY_FLAGS.VISIBLE); const xrayed = !!(flags & ENTITY_FLAGS.XRAYED); const highlighted = !!(flags & ENTITY_FLAGS.HIGHLIGHTED); const selected = !!(flags & ENTITY_FLAGS.SELECTED); const edges = !!(flags & ENTITY_FLAGS.EDGES); const pickable = !!(flags & ENTITY_FLAGS.PICKABLE); const culled = !!(flags & ENTITY_FLAGS.CULLED); // Color let f0; if (!visible || culled || xrayed || (highlighted && !this.model.scene.highlightMaterial.glowThrough) || (selected && !this.model.scene.selectedMaterial.glowThrough)) { f0 = RENDER_PASSES.NOT_RENDERED; } else { if (transparent) { f0 = RENDER_PASSES.COLOR_TRANSPARENT; } else { f0 = RENDER_PASSES.COLOR_OPAQUE; } } // Silhouette let f1; if (!visible || culled) { f1 = RENDER_PASSES.NOT_RENDERED; } else if (selected) { f1 = RENDER_PASSES.SILHOUETTE_SELECTED; } else if (highlighted) { f1 = RENDER_PASSES.SILHOUETTE_HIGHLIGHTED; } else if (xrayed) { f1 = RENDER_PASSES.SILHOUETTE_XRAYED; } else { f1 = RENDER_PASSES.NOT_RENDERED; } // Edges let f2 = 0; if (!visible || culled) { f2 = RENDER_PASSES.NOT_RENDERED; } else if (selected) { f2 = RENDER_PASSES.EDGES_SELECTED; } else if (highlighted) { f2 = RENDER_PASSES.EDGES_HIGHLIGHTED; } else if (xrayed) { f2 = RENDER_PASSES.EDGES_XRAYED; } else if (edges) { if (transparent) { f2 = RENDER_PASSES.EDGES_COLOR_TRANSPARENT; } else { f2 = RENDER_PASSES.EDGES_COLOR_OPAQUE; } } else { f2 = RENDER_PASSES.NOT_RENDERED; } // Pick let f3 = (visible && (!culled) && pickable) ? RENDER_PASSES.PICK : RENDER_PASSES.NOT_RENDERED; const textureState = this._dtxState; const gl = this.model.scene.canvas.gl; tempUint8Array4 [0] = f0; tempUint8Array4 [1] = f1; tempUint8Array4 [2] = f2; tempUint8Array4 [3] = f3; // object flags textureState.texturePerObjectColorsAndFlags._textureData.set(tempUint8Array4, subPortionId * 32 + 8); if (this._deferredSetFlagsActive || deferred) { this._deferredSetFlagsDirty = true; return; } if (++this._numUpdatesInFrame >= MAX_OBJECT_UPDATES_IN_FRAME_WITHOUT_BATCHED_UPDATE) { this._beginDeferredFlags(); // Subsequent flags updates now deferred } gl.bindTexture(gl.TEXTURE_2D, textureState.texturePerObjectColorsAndFlags._texture); gl.texSubImage2D( gl.TEXTURE_2D, 0, // level (subPortionId % 512) * 8 + 2, // xoffset Math.floor(subPortionId / 512), // yoffset 1, // width 1, //height gl.RGBA_INTEGER, gl.UNSIGNED_BYTE, tempUint8Array4 ); // gl.bindTexture (gl.TEXTURE_2D, null); } _setDeferredFlags() { } _setFlags2(portionId, flags, deferred = false) { const subPortionIds = this._portionToSubPortionsMap[portionId]; for (let i = 0, len = subPortionIds.length; i < len; i++) { this._subPortionSetFlags2(subPortionIds[i], flags, deferred); } } _subPortionSetFlags2(subPortionId, flags, deferred = false) { if (!this._finalized) { throw "Not finalized"; } const clippable = !!(flags & ENTITY_FLAGS.CLIPPABLE) ? 255 : 0; const textureState = this._dtxState; const gl = this.model.scene.canvas.gl; tempUint8Array4 [0] = clippable; tempUint8Array4 [1] = 0; tempUint8Array4 [2] = 1; tempUint8Array4 [3] = 2; // object flags2 textureState.texturePerObjectColorsAndFlags._textureData.set(tempUint8Array4, subPortionId * 32 + 12); if (this._deferredSetFlagsActive || deferred) { // console.log("_subPortionSetFlags2 set flags defer"); this._deferredSetFlagsDirty = true; return; } if (++this._numUpdatesInFrame >= MAX_OBJECT_UPDATES_IN_FRAME_WITHOUT_BATCHED_UPDATE) { this._beginDeferredFlags(); // Subsequent flags updates now deferred } gl.bindTexture(gl.TEXTURE_2D, textureState.texturePerObjectColorsAndFlags._texture); gl.texSubImage2D( gl.TEXTURE_2D, 0, // level (subPortionId % 512) * 8 + 3, // xoffset Math.floor(subPortionId / 512), // yoffset 1, // width 1, //height gl.RGBA_INTEGER, gl.UNSIGNED_BYTE, tempUint8Array4 ); // gl.bindTexture (gl.TEXTURE_2D, null); } _setDeferredFlags2() { } setOffset(portionId, offset) { const subPortionIds = this._portionToSubPortionsMap[portionId]; for (let i = 0, len = subPortionIds.length; i < len; i++) { this._subPortionSetOffset(subPortionIds[i], offset); } } _subPortionSetOffset(subPortionId, offset) { if (!this._finalized) { throw "Not finalized"; } // if (!this.model.scene.entityOffsetsEnabled) { // this.model.error("Entity#offset not enabled for this Viewer"); // See Viewer entityOffsetsEnabled // return; // } const textureState = this._dtxState; const gl = this.model.scene.canvas.gl; tempFloat32Array3 [0] = offset[0]; tempFloat32Array3 [1] = offset[1]; tempFloat32Array3 [2] = offset[2]; // object offset textureState.texturePerObjectOffsets._textureData.set(tempFloat32Array3, subPortionId * 3); if (this._deferredSetFlagsActive) { this._deferredSetFlagsDirty = true; return; } if (++this._numUpdatesInFrame >= MAX_OBJECT_UPDATES_IN_FRAME_WITHOUT_BATCHED_UPDATE) { this._beginDeferredFlags(); // Subsequent flags updates now deferred } gl.bindTexture(gl.TEXTURE_2D, textureState.texturePerObjectOffsets._texture); gl.texSubImage2D( gl.TEXTURE_2D, 0, // level 0, // x offset subPortionId, // yoffset 1, // width 1, // height gl.RGB, gl.FLOAT, tempFloat32Array3 ); // gl.bindTexture (gl.TEXTURE_2D, null); } setMatrix(portionId, matrix) { const subPortionIds = this._portionToSubPortionsMap[portionId]; for (let i = 0, len = subPortionIds.length; i < len; i++) { this._subPortionSetMatrix(subPortionIds[i], matrix); } } _subPortionSetMatrix(subPortionId, matrix) { if (!this._finalized) { throw "Not finalized"; } // if (!this.model.scene.entityMatrixsEnabled) { // this.model.error("Entity#matrix not enabled for this Viewer"); // See Viewer entityMatrixsEnabled // return; // } const textureState = this._dtxState; const gl = this.model.scene.canvas.gl; tempMat4a.set(matrix); textureState.texturePerObjectInstanceMatrices._textureData.set(tempMat4a, subPortionId * 16); if (this._deferredSetFlagsActive) { this._deferredSetFlagsDirty = true; return; } if (++this._numUpdatesInFrame >= MAX_OBJECT_UPDATES_IN_FRAME_WITHOUT_BATCHED_UPDATE) { this._beginDeferredFlags(); // Subsequent flags updates now deferred } gl.bindTexture(gl.TEXTURE_2D, textureState.texturePerObjectInstanceMatrices._texture); gl.texSubImage2D( gl.TEXTURE_2D, 0, // level (subPortionId % 512) * 4, // xoffset Math.floor(subPortionId / 512), // yoffset // 1, 4, // width 1, // height gl.RGBA, gl.FLOAT, tempMat4a ); // gl.bindTexture (gl.TEXTURE_2D, null); } getEachVertex(portionId, callback) { if (!this.model.scene.readableGeometryEnabled) { return; } const state = this._state; const subPortionIds = this._portionToSubPortionsMap[portionId]; if (!subPortionIds) { this.model.error("portion not found: " + portionId); return; } for (let i = 0, len = subPortionIds.length; i < len; i++) { const subPortionId = subPortionIds[i]; const subPortionReadableGeometry = this._subPortionReadableGeometries[subPortionId]; const positions = subPortionReadableGeometry.positionsCompressed; const positionsDecodeMatrix = subPortionReadableGeometry.positionsDecodeMatrix; subPortionReadableGeometry.meshMatrix; const origin = state.origin; const offsetX = origin[0] ; const offsetY = origin[1] ; const offsetZ = origin[2] ; const worldPos = tempVec4a$a; for (let i = 0, len = positions.length; i < len; i += 3) { worldPos[0] = positions[i]; worldPos[1] = positions[i + 1]; worldPos[2] = positions[i + 2]; worldPos[3] = 1.0; math.decompressPosition(worldPos, positionsDecodeMatrix); math.mulMat4v4(this.model.worldMatrix, worldPos, worldPos); worldPos[0] += offsetX; worldPos[1] += offsetY; worldPos[2] += offsetZ; callback(worldPos); } } } getEachIndex(portionId, callback) { if (!this.model.scene.readableGeometryEnabled) { return; } const subPortionIds = this._portionToSubPortionsMap[portionId]; if (!subPortionIds) { this.model.error("portion not found: " + portionId); return; } for (let i = 0, len = subPortionIds.length; i < len; i++) { const subPortionId = subPortionIds[i]; const subPortionReadableGeometry = this._subPortionReadableGeometries[subPortionId]; const indices = subPortionReadableGeometry.indices; for (let i = 0, len = indices.length; i < len; i++) { callback(indices[i]); } } } // ---------------------- COLOR RENDERING ----------------------------------- drawColorOpaque(renderFlags, frameCtx) { if (this._numCulledLayerPortions === this._numPortions || this._numVisibleLayerPortions === 0 || this._numTransparentLayerPortions === this._numPortions || this._numXRayedLayerPortions === this._numPortions) { return; } this._updateBackfaceCull(renderFlags, frameCtx); if (frameCtx.withSAO && this.model.saoEnabled) { if (this._renderers.colorRendererWithSAO) { this._renderers.colorRendererWithSAO.drawLayer(frameCtx, this, RENDER_PASSES.COLOR_OPAQUE); } } else { if (this._renderers.colorRenderer) { this._renderers.colorRenderer.drawLayer(frameCtx, this, RENDER_PASSES.COLOR_OPAQUE); } } } _updateBackfaceCull(renderFlags, frameCtx) { const backfaces = true; // See XCD-230 if (frameCtx.backfaces !== backfaces) { const gl = frameCtx.gl; { gl.disable(gl.CULL_FACE); } frameCtx.backfaces = backfaces; } } drawColorTransparent(renderFlags, frameCtx) { if (this._numCulledLayerPortions === this._numPortions || this._numVisibleLayerPortions === 0 || this._numTransparentLayerPortions === 0 || this._numXRayedLayerPortions === this._numPortions) { return; } this._updateBackfaceCull(renderFlags, frameCtx); if (this._renderers.colorRenderer) { this._renderers.colorRenderer.drawLayer(frameCtx, this, RENDER_PASSES.COLOR_TRANSPARENT); } } // ---------------------- RENDERING SAO POST EFFECT TARGETS -------------- drawDepth(renderFlags, frameCtx) { if (this._numCulledLayerPortions === this._numPortions || this._numVisibleLayerPortions === 0 || this._numTransparentLayerPortions === this._numPortions || this._numXRayedLayerPortions === this._numPortions) { return; } this._updateBackfaceCull(renderFlags, frameCtx); if (this._renderers.depthRenderer) { this._renderers.depthRenderer.drawLayer(frameCtx, this, RENDER_PASSES.COLOR_OPAQUE); // Assume whatever post-effect uses depth (eg SAO) does not apply to transparent objects } } drawNormals(renderFlags, frameCtx) { if (this._numCulledLayerPortions === this._numPortions || this._numVisibleLayerPortions === 0 || this._numTransparentLayerPortions === this._numPortions || this._numXRayedLayerPortions === this._numPortions) { return; } this._updateBackfaceCull(renderFlags, frameCtx); if (this._renderers.normalsRenderer) { this._renderers.normalsRenderer.drawLayer(frameCtx, this, RENDER_PASSES.COLOR_OPAQUE); // Assume whatever post-effect uses normals (eg SAO) does not apply to transparent objects } } // ---------------------- SILHOUETTE RENDERING ----------------------------------- drawSilhouetteXRayed(renderFlags, frameCtx) { if (this._numCulledLayerPortions === this._numPortions || this._numVisibleLayerPortions === 0 || this._numXRayedLayerPortions === 0) { return; } this._updateBackfaceCull(renderFlags, frameCtx); if (this._renderers.silhouetteRenderer) { this._renderers.silhouetteRenderer.drawLayer(frameCtx, this, RENDER_PASSES.SILHOUETTE_XRAYED); } } drawSilhouetteHighlighted(renderFlags, frameCtx) { if (this._numCulledLayerPortions === this._numPortions || this._numVisibleLayerPortions === 0 || this._numHighlightedLayerPortions === 0) { return; } this._updateBackfaceCull(renderFlags, frameCtx); if (this._renderers.silhouetteRenderer) { this._renderers.silhouetteRenderer.drawLayer(frameCtx, this, RENDER_PASSES.SILHOUETTE_HIGHLIGHTED); } } drawSilhouetteSelected(renderFlags, frameCtx) { if (this._numCulledLayerPortions === this._numPortions || this._numVisibleLayerPortions === 0 || this._numSelectedLayerPortions === 0) { return; } this._updateBackfaceCull(renderFlags, frameCtx); if (this._renderers.silhouetteRenderer) { this._renderers.silhouetteRenderer.drawLayer(frameCtx, this, RENDER_PASSES.SILHOUETTE_SELECTED); } } // ---------------------- EDGES RENDERING ----------------------------------- drawEdgesColorOpaque(renderFlags, frameCtx) { if (this.model.scene.logarithmicDepthBufferEnabled) { if (!this.model.scene._loggedWarning) { console.log("Edge enhancement for SceneModel data texture layers currently disabled with logarithmic depth buffer"); this.model.scene._loggedWarning = true; } return; } if (this._numCulledLayerPortions === this._numPortions || this._numVisibleLayerPortions === 0 || this._numEdgesLayerPortions === 0) { return; } if (this._renderers.edgesColorRenderer) { this._renderers.edgesColorRenderer.drawLayer(frameCtx, this, RENDER_PASSES.EDGES_COLOR_OPAQUE); } } drawEdgesColorTransparent(renderFlags, frameCtx) { if (this._numCulledLayerPortions === this._numPortions || this._numVisibleLayerPortions === 0 || this._numEdgesLayerPortions === 0 || this._numTransparentLayerPortions === 0) { return; } if (this._renderers.edgesColorRenderer) { this._renderers.edgesColorRenderer.drawLayer(frameCtx, this, RENDER_PASSES.EDGES_COLOR_TRANSPARENT); } } drawEdgesHighlighted(renderFlags, frameCtx) { if (this._numCulledLayerPortions === this._numPortions || this._numVisibleLayerPortions === 0 || this._numHighlightedLayerPortions === 0) { return; } if (this._renderers.edgesRenderer) { this._renderers.edgesRenderer.drawLayer(frameCtx, this, RENDER_PASSES.EDGES_HIGHLIGHTED); } } drawEdgesSelected(renderFlags, frameCtx) { if (this._numCulledLayerPortions === this._numPortions || this._numVisibleLayerPortions === 0 || this._numSelectedLayerPortions === 0) { return; } if (this._renderers.edgesRenderer) { this._renderers.edgesRenderer.drawLayer(frameCtx, this, RENDER_PASSES.EDGES_SELECTED); } } drawEdgesXRayed(renderFlags, frameCtx) { if (this._numCulledLayerPortions === this._numPortions || this._numVisibleLayerPortions === 0 || this._numXRayedLayerPortions === 0) { return; } if (this._renderers.edgesRenderer) { this._renderers.edgesRenderer.drawLayer(frameCtx, this, RENDER_PASSES.EDGES_XRAYED); } } // ---------------------- OCCLUSION CULL RENDERING ----------------------------------- drawOcclusion(renderFlags, frameCtx) { if (this._numCulledLayerPortions === this._numPortions || this._numVisibleLayerPortions === 0) { return; } this._updateBackfaceCull(renderFlags, frameCtx); if (this._renderers.occlusionRenderer) { this._renderers.occlusionRenderer.drawLayer(frameCtx, this, RENDER_PASSES.COLOR_OPAQUE); } } // ---------------------- SHADOW BUFFER RENDERING ----------------------------------- drawShadow(renderFlags, frameCtx) { if (this._numCulledLayerPortions === this._numPortions || this._numVisibleLayerPortions === 0) { return; } this._updateBackfaceCull(renderFlags, frameCtx); if (this._renderers.shadowRenderer) { this._renderers.shadowRenderer.drawLayer(frameCtx, this, RENDER_PASSES.COLOR_OPAQUE); } } //---- PICKING ---------------------------------------------------------------------------------------------------- setPickMatrices(pickViewMatrix, pickProjMatrix) { // if (this._numVisibleLayerPortions === 0) { // return; // } // this._dtxState.texturePickCameraMatrices.updateViewMatrix(pickViewMatrix, pickProjMatrix); } drawPickMesh(renderFlags, frameCtx) { if (this._numVisibleLayerPortions === 0) { return; } this._updateBackfaceCull(renderFlags, frameCtx); if (this._renderers.pickMeshRenderer) { this._renderers.pickMeshRenderer.drawLayer(frameCtx, this, RENDER_PASSES.PICK); } } drawPickDepths(renderFlags, frameCtx) { if (this._numVisibleLayerPortions === 0) { return; } this._updateBackfaceCull(renderFlags, frameCtx); if (this._renderers.pickDepthRenderer) { this._renderers.pickDepthRenderer.drawLayer(frameCtx, this, RENDER_PASSES.PICK); } } drawSnapInit(renderFlags, frameCtx) { if (this._numCulledLayerPortions === this._numPortions || this._numVisibleLayerPortions === 0) { return; } this._updateBackfaceCull(renderFlags, frameCtx); if (this._renderers.snapInitRenderer) { this._renderers.snapInitRenderer.drawLayer(frameCtx, this, RENDER_PASSES.PICK); } } drawSnap(renderFlags, frameCtx) { if (this._numCulledLayerPortions === this._numPortions || this._numVisibleLayerPortions === 0) { return; } this._updateBackfaceCull(renderFlags, frameCtx); if (this._renderers.snapRenderer) { this._renderers.snapRenderer.drawLayer(frameCtx, this, RENDER_PASSES.PICK); } } drawPickNormals(renderFlags, frameCtx) { if (this._numCulledLayerPortions === this._numPortions || this._numVisibleLayerPortions === 0) { return; } this._updateBackfaceCull(renderFlags, frameCtx); if (this._renderers.pickNormalsRenderer) { this._renderers.pickNormalsRenderer.drawLayer(frameCtx, this, RENDER_PASSES.PICK); } } destroy() { if (this._destroyed) { return; } const state = this._state; if (state.metallicRoughnessBuf) { state.metallicRoughnessBuf.destroy(); state.metallicRoughnessBuf = null; } this.model.scene.off(this._onSceneRendering); state.destroy(); this._destroyed = true; } } /** * A texture set within a {@link SceneModel}. * * * Created with {@link SceneModel#createTextureSet} * * Belongs to many {@link SceneModelMesh}es * * Stored by ID in {@link SceneModel#textureSets} * * Referenced by {@link SceneModelMesh#textureSet} */ class SceneModelTextureSet { /** * @private */ constructor(cfg) { /** * Unique ID of this SceneModelTextureSet. * * The SceneModelTextureSet is registered against this ID in {@link SceneModel#textureSets}. */ this.id = cfg.id; /** * The color texture. * @type {SceneModelTexture|*} */ this.colorTexture = cfg.colorTexture; /** * The alpha cutoff [float] */ this.alphaCutoff = cfg.alphaCutoff; /** * The metallic-roughness texture. * @type {SceneModelTexture|*} */ this.metallicRoughnessTexture = cfg.metallicRoughnessTexture; /** * The normal map texture. * @type {SceneModelTexture|*} */ this.normalsTexture = cfg.normalsTexture; /** * The emissive color texture. * @type {SceneModelTexture|*} */ this.emissiveTexture = cfg.emissiveTexture; /** * The ambient occlusion texture. * @type {SceneModelTexture|*} */ this.occlusionTexture = cfg.occlusionTexture; } /** * @private */ destroy() { } } /** * A texture within a {@link SceneModelTextureSet}. * * * Created with {@link SceneModel#createTexture} * * Belongs to many {@link SceneModelTextureSet}s * * Stored by ID in {@link SceneModel#textures}} */ class SceneModelTexture { /** * @private * @param cfg */ constructor(cfg) { /** * Unique ID of this SceneModelTexture. * * The SceneModelTexture is registered against this ID in {@link SceneModel#textures}. */ this.id = cfg.id; /** * @private */ this.texture = cfg.texture; } /** * @private */ destroy() { if (this.texture) { this.texture.destroy(); this.texture = null; } } } const Cache$1 = { enabled: false, files: {}, add: function (key, file) { if (this.enabled === false) { return; } this.files[key] = file; }, get: function (key) { if (this.enabled === false) { return; } return this.files[key]; }, remove: function (key) { delete this.files[key]; }, clear: function () { this.files = {}; } }; class LoadingManager { constructor(onLoad, onProgress, onError) { this.isLoading = false; this.itemsLoaded = 0; this.itemsTotal = 0; this.urlModifier = undefined; this.handlers = []; this.onStart = undefined; this.onLoad = onLoad; this.onProgress = onProgress; this.onError = onError; } itemStart(url) { this.itemsTotal++; if (this.isLoading === false) { if (this.onStart !== undefined) { this.onStart(url, this.itemsLoaded, this.itemsTotal); } } this.isLoading = true; } itemEnd(url) { this.itemsLoaded++; if (this.onProgress !== undefined) { this.onProgress(url, this.itemsLoaded, this.itemsTotal); } if (this.itemsLoaded === this.itemsTotal) { this.isLoading = false; if (this.onLoad !== undefined) { this.onLoad(); } } } itemError(url) { if (this.onError !== undefined) { this.onError(url); } } resolveURL(url) { if (this.urlModifier) { return this.urlModifier(url); } return url; } setURLModifier(transform) { this.urlModifier = transform; return this; } addHandler(regex, loader) { this.handlers.push(regex, loader); return this; } removeHandler(regex) { const index = this.handlers.indexOf(regex); if (index !== -1) { this.handlers.splice(index, 2); } return this; } getHandler(file) { for (let i = 0, l = this.handlers.length; i < l; i += 2) { const regex = this.handlers[i]; const loader = this.handlers[i + 1]; if (regex.global) regex.lastIndex = 0; // see #17920 if (regex.test(file)) { return loader; } } return null; } } const DefaultLoadingManager = new LoadingManager(); class Loader { constructor(manager) { this.manager = (manager !== undefined) ? manager : DefaultLoadingManager; this.crossOrigin = 'anonymous'; this.withCredentials = false; this.path = ''; this.resourcePath = ''; this.requestHeader = {}; } load( /* url, onLoad, onProgress, onError */) { } loadAsync(url, onProgress) { const scope = this; return new Promise(function (resolve, reject) { scope.load(url, resolve, onProgress, reject); }); } parse( /* data */) { } setCrossOrigin(crossOrigin) { this.crossOrigin = crossOrigin; return this; } setWithCredentials(value) { this.withCredentials = value; return this; } setPath(path) { this.path = path; return this; } setResourcePath(resourcePath) { this.resourcePath = resourcePath; return this; } setRequestHeader(requestHeader) { this.requestHeader = requestHeader; return this; } } const loading = {}; class FileLoader extends Loader { constructor(manager) { super(manager); } load(url, onLoad, onProgress, onError) { if (url === undefined) { url = ''; } if (this.path !== undefined) { url = this.path + url; } url = this.manager.resolveURL(url); const cached = Cache$1.get(url); if (cached !== undefined) { this.manager.itemStart(url); core.scheduleTask(() => { if (onLoad) { onLoad(cached); } this.manager.itemEnd(url); }, 0); return cached; } if (loading[url] !== undefined) { loading[url].push({onLoad, onProgress, onError}); return; } loading[url] = []; loading[url].push({onLoad, onProgress, onError}); const req = new Request(url, { headers: new Headers(this.requestHeader), credentials: this.withCredentials ? 'include' : 'same-origin' }); const mimeType = this.mimeType; const responseType = this.responseType; fetch(req).then(response => { if (response.status === 200 || response.status === 0) { // Some browsers return HTTP Status 0 when using non-http protocol // e.g. 'file://' or 'data://'. Handle as success. if (response.status === 0) { console.warn('FileLoader: HTTP Status 0 received.'); } if (typeof ReadableStream === 'undefined' || response.body.getReader === undefined) { return response; } const callbacks = loading[url]; const reader = response.body.getReader(); const contentLength = response.headers.get('Content-Length'); const total = contentLength ? parseInt(contentLength) : 0; const lengthComputable = total !== 0; let loaded = 0; const stream = new ReadableStream({ start(controller) { readData(); function readData() { reader.read().then(({done, value}) => { if (done) { controller.close(); } else { loaded += value.byteLength; const event = new ProgressEvent('progress', {lengthComputable, loaded, total}); for (let i = 0, il = callbacks.length; i < il; i++) { const callback = callbacks[i]; if (callback.onProgress) { callback.onProgress(event); } } controller.enqueue(value); readData(); } }); } } }); return new Response(stream); } else { throw Error(`fetch for "${response.url}" responded with ${response.status}: ${response.statusText}`); } }).then(response => { switch (responseType) { case 'arraybuffer': return response.arrayBuffer(); case 'blob': return response.blob(); case 'document': return response.text() .then(text => { const parser = new DOMParser(); return parser.parseFromString(text, mimeType); }); case 'json': return response.json(); default: if (mimeType === undefined) { return response.text(); } else { // sniff encoding const re = /charset="?([^;"\s]*)"?/i; const exec = re.exec(mimeType); const label = exec && exec[1] ? exec[1].toLowerCase() : undefined; const decoder = new TextDecoder(label); return response.arrayBuffer().then(ab => decoder.decode(ab)); } } }).then(data => { // Add to cache only on HTTP success, so that we do not cache // error response bodies as proper responses to requests. Cache$1.add(url, data); const callbacks = loading[url]; delete loading[url]; for (let i = 0, il = callbacks.length; i < il; i++) { const callback = callbacks[i]; if (callback.onLoad) { callback.onLoad(data); } } }).catch(err => { // Abort errors and other errors are handled the same const callbacks = loading[url]; if (callbacks === undefined) { // When onLoad was called and url was deleted in `loading` this.manager.itemError(url); throw err; } delete loading[url]; for (let i = 0, il = callbacks.length; i < il; i++) { const callback = callbacks[i]; if (callback.onError) { callback.onError(err); } } this.manager.itemError(url); }).finally(() => { this.manager.itemEnd(url); }); this.manager.itemStart(url); } setResponseType(value) { this.responseType = value; return this; } setMimeType(value) { this.mimeType = value; return this; } } /** * @author Deepkolos / https://github.com/deepkolos */ class WorkerPool$1 { constructor(pool = 4) { this.pool = pool; this.queue = []; this.workers = []; this.workersResolve = []; this.workerStatus = 0; } _initWorker(workerId) { if (!this.workers[workerId]) { const worker = this.workerCreator(); worker.addEventListener('message', this._onMessage.bind(this, workerId)); this.workers[workerId] = worker; } } _getIdleWorker() { for (let i = 0; i < this.pool; i++) if (!(this.workerStatus & (1 << i))) return i; return -1; } _onMessage(workerId, msg) { const resolve = this.workersResolve[workerId]; resolve && resolve(msg); if (this.queue.length) { const {resolve, msg, transfer} = this.queue.shift(); this.workersResolve[workerId] = resolve; this.workers[workerId].postMessage(msg, transfer); } else { this.workerStatus ^= 1 << workerId; } } setWorkerCreator(workerCreator) { this.workerCreator = workerCreator; } setWorkerLimit(pool) { this.pool = pool; } postMessage(msg, transfer) { return new Promise((resolve) => { const workerId = this._getIdleWorker(); if (workerId !== -1) { this._initWorker(workerId); this.workerStatus |= 1 << workerId; this.workersResolve[workerId] = resolve; this.workers[workerId].postMessage(msg, transfer); } else { this.queue.push({resolve, msg, transfer}); } }); } destroy() { this.workers.forEach((worker) => worker.terminate()); this.workersResolve.length = 0; this.workers.length = 0; this.queue.length = 0; this.workerStatus = 0; } } const KTX2TransferSRGB = 2; const KTX2_ALPHA_PREMULTIPLIED = 1; let activeTranscoders = 0; /** * Transcodes texture data from KTX2. * * ## Overview * * * Uses the [Basis Universal GPU Texture Codec](https://github.com/BinomialLLC/basis_universal) to * transcode [KTX2](https://github.khronos.org/KTX-Specification/) textures. * * {@link XKTLoaderPlugin} uses a KTX2TextureTranscoder to load textures in XKT files. * * {@link VBOSceneModel} uses a KTX2TextureTranscoder to enable us to add KTX2-encoded textures. * * Loads the Basis Codec from [CDN](https://cdn.jsdelivr.net/npm/@xeokit/xeokit-sdk/dist/basis/) by default, but can * also be configured to load the Codec from local files. * * We also bundle the Basis Codec with the xeokit-sdk npm package, and in the [repository](https://github.com/xeokit/xeokit-sdk/tree/master/dist/basis). * * ## What is KTX2? * * A [KTX2](https://github.khronos.org/KTX-Specification/) file stores GPU texture data in the Khronos Texture 2.0 (KTX2) container format. It contains image data for * a texture asset compressed with Basis Universal (BasisU) supercompression that can be transcoded to different formats * depending on the support provided by the target devices. KTX2 provides a lightweight format for distributing texture * assets to GPUs. Due to BasisU compression, KTX2 files can store any image format supported by GPUs. * * ## Loading XKT files containing KTX2 textures * * {@link XKTLoaderPlugin} uses a KTX2TextureTranscoder to load textures in XKT files. An XKTLoaderPlugin has its own * default KTX2TextureTranscoder, configured to load the Basis Codec from the [CDN](https://cdn.jsdelivr.net/npm/@xeokit/xeokit-sdk/dist/basis/). If we wish, we can override that with our own * KTX2TextureTranscoder, configured to load the Codec locally. * * In the example below, we'll create a {@link Viewer} and add an {@link XKTLoaderPlugin} * configured with a KTX2TextureTranscoder. Then we'll use the XKTLoaderPlugin to load an * XKT file that contains KTX2 textures, which the plugin will transcode using * its KTX2TextureTranscoder. * * We'll configure our KTX2TextureTranscoder to load the Basis Codec from a local directory. If we were happy with loading the * Codec from our [CDN](https://cdn.jsdelivr.net/npm/@xeokit/xeokit-sdk/dist/basis/) (ie. our app will always have an Internet connection) then we could just leave out the * KTX2TextureTranscoder altogether, and let the XKTLoaderPlugin use its internal default KTX2TextureTranscoder, which is configured to * load the Codec from the CDN. We'll stick with loading our own Codec, in case we want to run our app without an Internet connection. * * * * * [[Run this example](https://xeokit.github.io/xeokit-sdk/examples/buildings/#xkt_vbo_textures_HousePlan)] * * ````javascript * const viewer = new Viewer({ * canvasId: "myCanvas", * transparent: true * }); * * viewer.camera.eye = [-2.56, 8.38, 8.27]; * viewer.camera.look = [13.44, 3.31, -14.83]; * viewer.camera.up = [0.10, 0.98, -0.14]; * * const textureTranscoder = new KTX2TextureTranscoder({ * viewer, * transcoderPath: "https://cdn.jsdelivr.net/npm/@xeokit/xeokit-sdk/dist/basis/" // <------ Path to Basis Universal transcoder * }); * * const xktLoader = new XKTLoaderPlugin(viewer, { * textureTranscoder // <<------------- Transcodes KTX2 textures in XKT files * }); * * const sceneModel = xktLoader.load({ * id: "myModel", * src: "./HousePlan.xkt" // <<------ XKT file with KTX2 textures * }); * ```` * * ## Loading KTX2 files into a VBOSceneModel * * A {@link SceneModel} that is configured with a KTX2TextureTranscoder will * allow us to load textures into it from KTX2-transcoded buffers or files. * * In the example below, we'll create a {@link Viewer}, containing a {@link VBOSceneModel} configured with a * KTX2TextureTranscoder. * * We'll then programmatically create a simple object within the VBOSceneModel, consisting of * a single box mesh with a texture loaded from a KTX2 file, which our VBOSceneModel internally transcodes, using * its KTX2TextureTranscoder. * * As in the previous example, we'll configure our KTX2TextureTranscoder to load the Basis Codec from a local directory. * * * [Run a similar example](https://xeokit.github.io/xeokit-sdk/examples/scenemodel/#vbo_batching_autocompressed_triangles_textures_ktx2) * * ````javascript * const viewer = new Viewer({ * canvasId: "myCanvas", * transparent: true * }); * * viewer.scene.camera.eye = [-21.80, 4.01, 6.56]; * viewer.scene.camera.look = [0, -5.75, 0]; * viewer.scene.camera.up = [0.37, 0.91, -0.11]; * * const textureTranscoder = new KTX2TextureTranscoder({ * viewer, * transcoderPath: "https://cdn.jsdelivr.net/npm/@xeokit/xeokit-sdk/dist/basis/" // <------ Path to BasisU transcoder module * }); * * const vboSceneModel = new VBOSceneModel(viewer.scene, { * id: "myModel", * textureTranscoder // <<-------------------- Configure model with our transcoder * }); * * vboSceneModel.createTexture({ * id: "myColorTexture", * src: "../assets/textures/compressed/sample_uastc_zstd.ktx2" // <<----- KTX2 texture asset * }); * * vboSceneModel.createTexture({ * id: "myMetallicRoughnessTexture", * src: "../assets/textures/alpha/crosshatchAlphaMap.jpg" // <<----- JPEG texture asset * }); * * vboSceneModel.createTextureSet({ * id: "myTextureSet", * colorTextureId: "myColorTexture", * metallicRoughnessTextureId: "myMetallicRoughnessTexture" * }); * * vboSceneModel.createMesh({ * id: "myMesh", * textureSetId: "myTextureSet", * primitive: "triangles", * positions: [1, 1, 1, ...], * normals: [0, 0, 1, 0, ...], * uv: [1, 0, 0, ...], * indices: [0, 1, 2, ...], * }); * * vboSceneModel.createEntity({ * id: "myEntity", * meshIds: ["myMesh"] * }); * * vboSceneModel.finalize(); * ```` * * ## Loading KTX2 ArrayBuffers into a VBOSceneModel * * A {@link SceneModel} that is configured with a KTX2TextureTranscoder will also allow us to load textures into * it from KTX2 ArrayBuffers. * * In the example below, we'll create a {@link Viewer}, containing a {@link VBOSceneModel} configured with a * KTX2TextureTranscoder. * * We'll then programmatically create a simple object within the VBOSceneModel, consisting of * a single mesh with a texture loaded from a KTX2 ArrayBuffer, which our VBOSceneModel internally transcodes, using * its KTX2TextureTranscoder. * * ````javascript * const viewer = new Viewer({ * canvasId: "myCanvas", * transparent: true * }); * * viewer.scene.camera.eye = [-21.80, 4.01, 6.56]; * viewer.scene.camera.look = [0, -5.75, 0]; * viewer.scene.camera.up = [0.37, 0.91, -0.11]; * * const textureTranscoder = new KTX2TextureTranscoder({ * viewer, * transcoderPath: "https://cdn.jsdelivr.net/npm/@xeokit/xeokit-sdk/dist/basis/" // <------ Path to BasisU transcoder module * }); * * const vboSceneModel = new VBOSceneModel(viewer.scene, { * id: "myModel", * textureTranscoder // <<-------------------- Configure model with our transcoder * }); * * utils.loadArraybuffer("../assets/textures/compressed/sample_uastc_zstd.ktx2",(arrayBuffer) => { * * vboSceneModel.createTexture({ * id: "myColorTexture", * buffers: [arrayBuffer] // <<----- KTX2 texture asset * }); * * vboSceneModel.createTexture({ * id: "myMetallicRoughnessTexture", * src: "../assets/textures/alpha/crosshatchAlphaMap.jpg" // <<----- JPEG texture asset * }); * * vboSceneModel.createTextureSet({ * id: "myTextureSet", * colorTextureId: "myColorTexture", * metallicRoughnessTextureId: "myMetallicRoughnessTexture" * }); * * vboSceneModel.createMesh({ * id: "myMesh", * textureSetId: "myTextureSet", * primitive: "triangles", * positions: [1, 1, 1, ...], * normals: [0, 0, 1, 0, ...], * uv: [1, 0, 0, ...], * indices: [0, 1, 2, ...], * }); * * vboSceneModel.createEntity({ * id: "myEntity", * meshIds: ["myMesh"] * }); * * vboSceneModel.finalize(); * }); * ```` * * @implements {TextureTranscoder} */ class KTX2TextureTranscoder { /** * Creates a new KTX2TextureTranscoder. * * @param {Viewer} viewer The Viewer that our KTX2TextureTranscoder will be used with. This KTX2TextureTranscoder * must only be used to transcode textures for this Viewer. This is because the Viewer's capabilities will decide * what target GPU formats this KTX2TextureTranscoder will transcode to. * @param {String} [transcoderPath="https://cdn.jsdelivr.net/npm/@xeokit/xeokit-sdk/dist/basis/"] Path to the Basis * transcoder module that internally does the heavy lifting for our KTX2TextureTranscoder. If we omit this configuration, * then our KTX2TextureTranscoder will load it from ````https://cdn.jsdelivr.net/npm/@xeokit/xeokit-sdk/dist/basis/```` by * default. Therefore, make sure your application is connected to the internet if you wish to use the default transcoder path. * @param {Number} [workerLimit] The maximum number of Workers to use for transcoding. */ constructor({viewer, transcoderPath, workerLimit}) { this._transcoderPath = transcoderPath || "https://cdn.jsdelivr.net/npm/@xeokit/xeokit-sdk/dist/basis/"; this._transcoderBinary = null; this._transcoderPending = null; this._workerPool = new WorkerPool$1(); this._workerSourceURL = ''; if (workerLimit) { this._workerPool.setWorkerLimit(workerLimit); } const viewerCapabilities = viewer.capabilities; this._workerConfig = { astcSupported: viewerCapabilities.astcSupported, etc1Supported: viewerCapabilities.etc1Supported, etc2Supported: viewerCapabilities.etc2Supported, dxtSupported: viewerCapabilities.dxtSupported, bptcSupported: viewerCapabilities.bptcSupported, pvrtcSupported: viewerCapabilities.pvrtcSupported }; this._supportedFileTypes = ["xkt2"]; } _init() { if (!this._transcoderPending) { const jsLoader = new FileLoader(); jsLoader.setPath(this._transcoderPath); jsLoader.setWithCredentials(this.withCredentials); const jsContent = jsLoader.loadAsync('basis_transcoder.js'); const binaryLoader = new FileLoader(); binaryLoader.setPath(this._transcoderPath); binaryLoader.setResponseType('arraybuffer'); binaryLoader.setWithCredentials(this.withCredentials); const binaryContent = binaryLoader.loadAsync('basis_transcoder.wasm'); this._transcoderPending = Promise.all([jsContent, binaryContent]) .then(([jsContent, binaryContent]) => { const fn = KTX2TextureTranscoder.BasisWorker.toString(); const body = [ '/* constants */', 'let _EngineFormat = ' + JSON.stringify(KTX2TextureTranscoder.EngineFormat), 'let _TranscoderFormat = ' + JSON.stringify(KTX2TextureTranscoder.TranscoderFormat), 'let _BasisFormat = ' + JSON.stringify(KTX2TextureTranscoder.BasisFormat), '/* basis_transcoder.js */', jsContent, '/* worker */', fn.substring(fn.indexOf('{') + 1, fn.lastIndexOf('}')) ].join('\n'); this._workerSourceURL = URL.createObjectURL(new Blob([body])); this._transcoderBinary = binaryContent; this._workerPool.setWorkerCreator(() => { const worker = new Worker(this._workerSourceURL); const transcoderBinary = this._transcoderBinary.slice(0); worker.postMessage({ type: 'init', config: this._workerConfig, transcoderBinary }, [transcoderBinary]); return worker; }); }); if (activeTranscoders > 0) { console.warn('KTX2TextureTranscoder: Multiple active KTX2TextureTranscoder may cause performance issues.' + ' Use a single KTX2TextureTranscoder instance, or call .dispose() on old instances.'); } activeTranscoders++; } return this._transcoderPending; } /** * Transcodes texture data from transcoded buffers into a {@link Texture2D}. * * @param {ArrayBuffer[]} buffers Transcoded texture data. Given as an array of buffers so that we can support multi-image textures, such as cube maps. * @param {*} config Transcoding options. * @param {Texture2D} texture The texture to load. * @returns {Promise} Resolves when the texture has loaded. */ transcode(buffers, texture, config = {}) { return new Promise((resolve, reject) => { const taskConfig = config; this._init().then(() => { return this._workerPool.postMessage({ type: 'transcode', buffers, taskConfig: taskConfig }, buffers); }).then((e) => { const transcodeResult = e.data; const {mipmaps, width, height, format, type, error, dfdTransferFn, dfdFlags} = transcodeResult; if (type === 'error') { return reject(error); } texture.setCompressedData({ mipmaps, props: { format: format, minFilter: mipmaps.length === 1 ? LinearFilter : LinearMipmapLinearFilter, magFilter: mipmaps.length === 1 ? LinearFilter : LinearMipmapLinearFilter, encoding: dfdTransferFn === KTX2TransferSRGB ? sRGBEncoding : LinearEncoding, premultiplyAlpha: !!(dfdFlags & KTX2_ALPHA_PREMULTIPLIED) } }); resolve(); }); }); } /** * Destroys this KTX2TextureTranscoder */ destroy() { URL.revokeObjectURL(this._workerSourceURL); this._workerPool.destroy(); activeTranscoders--; } } /** * @private */ KTX2TextureTranscoder.BasisFormat = { ETC1S: 0, UASTC_4x4: 1 }; /** * @private */ KTX2TextureTranscoder.TranscoderFormat = { ETC1: 0, ETC2: 1, BC1: 2, BC3: 3, BC4: 4, BC5: 5, BC7_M6_OPAQUE_ONLY: 6, BC7_M5: 7, PVRTC1_4_RGB: 8, PVRTC1_4_RGBA: 9, ASTC_4x4: 10, ATC_RGB: 11, ATC_RGBA_INTERPOLATED_ALPHA: 12, RGBA32: 13, RGB565: 14, BGR565: 15, RGBA4444: 16 }; /** * @private */ KTX2TextureTranscoder.EngineFormat = { RGBAFormat: RGBAFormat, RGBA_ASTC_4x4_Format: RGBA_ASTC_4x4_Format, RGBA_BPTC_Format: RGBA_BPTC_Format, RGBA_ETC2_EAC_Format: RGBA_ETC2_EAC_Format, RGBA_PVRTC_4BPPV1_Format: RGBA_PVRTC_4BPPV1_Format, RGBA_S3TC_DXT5_Format: RGBA_S3TC_DXT5_Format, RGB_ETC1_Format: RGB_ETC1_Format, RGB_ETC2_Format: RGB_ETC2_Format, RGB_PVRTC_4BPPV1_Format: RGB_PVRTC_4BPPV1_Format, RGB_S3TC_DXT1_Format: RGB_S3TC_DXT1_Format }; /* WEB WORKER */ /** * @private * @constructor */ KTX2TextureTranscoder.BasisWorker = function () { let config; let transcoderPending; let BasisModule; const EngineFormat = _EngineFormat; // eslint-disable-line no-undef const TranscoderFormat = _TranscoderFormat; // eslint-disable-line no-undef const BasisFormat = _BasisFormat; // eslint-disable-line no-undef self.addEventListener('message', function (e) { const message = e.data; switch (message.type) { case 'init': config = message.config; init(message.transcoderBinary); break; case 'transcode': transcoderPending.then(() => { try { const { width, height, hasAlpha, mipmaps, format, dfdTransferFn, dfdFlags } = transcode(message.buffers[0]); const buffers = []; for (let i = 0; i < mipmaps.length; ++i) { buffers.push(mipmaps[i].data.buffer); } self.postMessage({ type: 'transcode', id: message.id, width, height, hasAlpha, mipmaps, format, dfdTransferFn, dfdFlags }, buffers); } catch (error) { console.error(`[KTX2TextureTranscoder.BasisWorker]: ${error}`); self.postMessage({type: 'error', id: message.id, error: error.message}); } }); break; } }); function init(wasmBinary) { transcoderPending = new Promise(resolve => { BasisModule = { wasmBinary, onRuntimeInitialized: resolve }; BASIS(BasisModule); // eslint-disable-line no-undef }).then(() => { BasisModule.initializeBasis(); if (BasisModule.KTX2File === undefined) { console.warn('KTX2TextureTranscoder: Please update Basis Universal transcoder.'); } }); } function transcode(buffer) { const ktx2File = new BasisModule.KTX2File(new Uint8Array(buffer)); function cleanup() { ktx2File.close(); ktx2File.delete(); } if (!ktx2File.isValid()) { cleanup(); throw new Error('KTX2TextureTranscoder: Invalid or unsupported .ktx2 file'); } const basisFormat = ktx2File.isUASTC() ? BasisFormat.UASTC_4x4 : BasisFormat.ETC1S; const width = ktx2File.getWidth(); const height = ktx2File.getHeight(); const levels = ktx2File.getLevels(); const hasAlpha = ktx2File.getHasAlpha(); const dfdTransferFn = ktx2File.getDFDTransferFunc(); const dfdFlags = ktx2File.getDFDFlags(); const {transcoderFormat, engineFormat} = getTranscoderFormat(basisFormat, width, height, hasAlpha); if (!width || !height || !levels) { cleanup(); throw new Error('KTX2TextureTranscoder: Invalid texture'); } if (!ktx2File.startTranscoding()) { cleanup(); throw new Error('KTX2TextureTranscoder: .startTranscoding failed'); } const mipmaps = []; for (let mip = 0; mip < levels; mip++) { const levelInfo = ktx2File.getImageLevelInfo(mip, 0, 0); const mipWidth = levelInfo.origWidth; const mipHeight = levelInfo.origHeight; const dst = new Uint8Array(ktx2File.getImageTranscodedSizeInBytes(mip, 0, 0, transcoderFormat)); const status = ktx2File.transcodeImage(dst, mip, 0, 0, transcoderFormat, 0, -1, -1); if (!status) { cleanup(); throw new Error('KTX2TextureTranscoder: .transcodeImage failed.'); } mipmaps.push({data: dst, width: mipWidth, height: mipHeight}); } cleanup(); return {width, height, hasAlpha, mipmaps, format: engineFormat, dfdTransferFn, dfdFlags}; } // Optimal choice of a transcoder target format depends on the Basis format (ETC1S or UASTC), // device capabilities, and texture dimensions. The list below ranks the formats separately // for ETC1S and UASTC. // // In some cases, transcoding UASTC to RGBA32 might be preferred for higher quality (at // significant memory cost) compared to ETC1/2, BC1/3, and PVRTC. The transcoder currently // chooses RGBA32 only as a last resort and does not expose that option to the caller. const FORMAT_OPTIONS = [{ if: 'astcSupported', basisFormat: [BasisFormat.UASTC_4x4], transcoderFormat: [TranscoderFormat.ASTC_4x4, TranscoderFormat.ASTC_4x4], engineFormat: [EngineFormat.RGBA_ASTC_4x4_Format, EngineFormat.RGBA_ASTC_4x4_Format], priorityETC1S: Infinity, priorityUASTC: 1, needsPowerOfTwo: false }, { if: 'bptcSupported', basisFormat: [BasisFormat.ETC1S, BasisFormat.UASTC_4x4], transcoderFormat: [TranscoderFormat.BC7_M5, TranscoderFormat.BC7_M5], engineFormat: [EngineFormat.RGBA_BPTC_Format, EngineFormat.RGBA_BPTC_Format], priorityETC1S: 3, priorityUASTC: 2, needsPowerOfTwo: false }, { if: 'dxtSupported', basisFormat: [BasisFormat.ETC1S, BasisFormat.UASTC_4x4], transcoderFormat: [TranscoderFormat.BC1, TranscoderFormat.BC3], engineFormat: [EngineFormat.RGB_S3TC_DXT1_Format, EngineFormat.RGBA_S3TC_DXT5_Format], priorityETC1S: 4, priorityUASTC: 5, needsPowerOfTwo: false }, { if: 'etc2Supported', basisFormat: [BasisFormat.ETC1S, BasisFormat.UASTC_4x4], transcoderFormat: [TranscoderFormat.ETC1, TranscoderFormat.ETC2], engineFormat: [EngineFormat.RGB_ETC2_Format, EngineFormat.RGBA_ETC2_EAC_Format], priorityETC1S: 1, priorityUASTC: 3, needsPowerOfTwo: false }, { if: 'etc1Supported', basisFormat: [BasisFormat.ETC1S, BasisFormat.UASTC_4x4], transcoderFormat: [TranscoderFormat.ETC1], engineFormat: [EngineFormat.RGB_ETC1_Format], priorityETC1S: 2, priorityUASTC: 4, needsPowerOfTwo: false }, { if: 'pvrtcSupported', basisFormat: [BasisFormat.ETC1S, BasisFormat.UASTC_4x4], transcoderFormat: [TranscoderFormat.PVRTC1_4_RGB, TranscoderFormat.PVRTC1_4_RGBA], engineFormat: [EngineFormat.RGB_PVRTC_4BPPV1_Format, EngineFormat.RGBA_PVRTC_4BPPV1_Format], priorityETC1S: 5, priorityUASTC: 6, needsPowerOfTwo: true }]; const ETC1S_OPTIONS = FORMAT_OPTIONS.sort(function (a, b) { return a.priorityETC1S - b.priorityETC1S; }); const UASTC_OPTIONS = FORMAT_OPTIONS.sort(function (a, b) { return a.priorityUASTC - b.priorityUASTC; }); function getTranscoderFormat(basisFormat, width, height, hasAlpha) { let transcoderFormat; let engineFormat; const options = basisFormat === BasisFormat.ETC1S ? ETC1S_OPTIONS : UASTC_OPTIONS; for (let i = 0; i < options.length; i++) { const opt = options[i]; if (!config[opt.if]) continue; if (!opt.basisFormat.includes(basisFormat)) continue; if (hasAlpha && opt.transcoderFormat.length < 2) continue; if (opt.needsPowerOfTwo && !(isPowerOfTwo(width) && isPowerOfTwo(height))) continue; transcoderFormat = opt.transcoderFormat[hasAlpha ? 1 : 0]; engineFormat = opt.engineFormat[hasAlpha ? 1 : 0]; return { transcoderFormat, engineFormat }; } console.warn('KTX2TextureTranscoder: No suitable compressed texture format found. Decoding to RGBA32.'); transcoderFormat = TranscoderFormat.RGBA32; engineFormat = EngineFormat.RGBAFormat; return { transcoderFormat, engineFormat }; } function isPowerOfTwo(value) { if (value <= 2) return true; return (value & value - 1) === 0 && value !== 0; } }; const cachedTranscoders = {}; /** * Returns a new {@link KTX2TextureTranscoder}. * * The ````transcoderPath```` config will be set to: "https://cdn.jsdelivr.net/npm/@xeokit/xeokit-sdk/dist/basis/" * * @private */ function getKTX2TextureTranscoder(viewer) { const sceneId = viewer.scene.id; let transcoder = cachedTranscoders[sceneId]; if (!transcoder) { transcoder = new KTX2TextureTranscoder({viewer}); cachedTranscoders[sceneId] = transcoder; viewer.scene.on("destroyed", () => { delete cachedTranscoders[sceneId]; transcoder.destroy(); }); } return transcoder; } /** * @author https://github.com/tmarti, with support from https://tribia.com/ * @license MIT * * This file takes a geometry given by { positionsCompressed, indices }, and returns * equivalent { positionsCompressed, indices } arrays but which only contain unique * positionsCompressed. * * The time is O(N logN) with the number of positionsCompressed due to a pre-sorting * step, but is much more GC-friendly and actually faster than the classic O(N) * approach based in keeping a hash-based LUT to identify unique positionsCompressed. */ let comparePositions = null; function compareVertex(a, b) { let res; for (let i = 0; i < 3; i++) { if (0 !== (res = comparePositions[a * 3 + i] - comparePositions[b * 3 + i])) { return res; } } return 0; } let seqInit = null; function setMaxNumberOfPositions(maxPositions) { if (seqInit !== null && seqInit.length >= maxPositions) { return; } seqInit = new Uint32Array(maxPositions); for (let i = 0; i < maxPositions; i++) { seqInit[i] = i; } } /** * This function obtains unique positionsCompressed in the provided object * .positionsCompressed array and calculates an index mapping, which is then * applied to the provided object .indices and .edgeindices. * * The input object items are not modified, and instead new set * of positionsCompressed, indices and edgeIndices with the applied optimization * are returned. * * The algorithm, instead of being based in a hash-like LUT for * identifying unique positionsCompressed, is based in pre-sorting the input * positionsCompressed... * * (it's possible to define a _"consistent ordering"_ for the positionsCompressed * as positionsCompressed are quantized and thus not suffer from float number * comparison artifacts) * * ... so same positionsCompressed are adjacent in the sorted array, and then * it's easy to scan linearly the sorted array. During the linear run, * we will know that we found a different position because the comparison * function will return != 0 between current and previous element. * * During this linear traversal of the array, a `unique counter` is used * in order to calculate the mapping between original indices and unique * indices. * * @param {{positionsCompressed: number[],indices: number[], edgeIndices: number[]}} mesh The input mesh to process, with `positionsCompressed`, `indices` and `edgeIndices` keys. * * @returns {[Uint16Array, Uint32Array, Uint32Array]} An array with 3 elements: 0 => the uniquified positionsCompressed; 1 and 2 => the remapped edges and edgeIndices arrays */ function uniquifyPositions(mesh) { const _positions = mesh.positionsCompressed; const _indices = mesh.indices; const _edgeIndices = mesh.edgeIndices; setMaxNumberOfPositions(_positions.length / 3); const seq = seqInit.slice(0, _positions.length / 3); const remappings = seqInit.slice(0, _positions.length / 3); comparePositions = _positions; seq.sort(compareVertex); let uniqueIdx = 0; remappings[seq[0]] = 0; for (let i = 1, len = seq.length; i < len; i++) { if (0 !== compareVertex(seq[i], seq[i - 1])) { uniqueIdx++; } remappings[seq[i]] = uniqueIdx; } const numUniquePositions = uniqueIdx + 1; const newPositions = new Uint16Array(numUniquePositions * 3); uniqueIdx = 0; newPositions [uniqueIdx * 3 + 0] = _positions [seq[0] * 3 + 0]; newPositions [uniqueIdx * 3 + 1] = _positions [seq[0] * 3 + 1]; newPositions [uniqueIdx * 3 + 2] = _positions [seq[0] * 3 + 2]; for (let i = 1, len = seq.length; i < len; i++) { if (0 !== compareVertex(seq[i], seq[i - 1])) { uniqueIdx++; newPositions [uniqueIdx * 3 + 0] = _positions [seq[i] * 3 + 0]; newPositions [uniqueIdx * 3 + 1] = _positions [seq[i] * 3 + 1]; newPositions [uniqueIdx * 3 + 2] = _positions [seq[i] * 3 + 2]; } remappings[seq[i]] = uniqueIdx; } comparePositions = null; const newIndices = new Uint32Array(_indices.length); for (let i = 0, len = _indices.length; i < len; i++) { newIndices[i] = remappings [_indices[i]]; } const newEdgeIndices = new Uint32Array(_edgeIndices.length); for (let i = 0, len = _edgeIndices.length; i < len; i++) { newEdgeIndices[i] = remappings [_edgeIndices[i]]; } return [newPositions, newIndices, newEdgeIndices]; } /** * @author https://github.com/tmarti, with support from https://tribia.com/ * @license MIT **/ const MAX_RE_BUCKET_FAN_OUT = 8; let bucketsForIndices = null; function compareBuckets(a, b) { const aa = a * 3; const bb = b * 3; let aa1, aa2, aa3, bb1, bb2, bb3; const minBucketA = Math.min( aa1 = bucketsForIndices[aa], aa2 = bucketsForIndices[aa + 1], aa3 = bucketsForIndices[aa + 2] ); const minBucketB = Math.min( bb1 = bucketsForIndices[bb], bb2 = bucketsForIndices[bb + 1], bb3 = bucketsForIndices[bb + 2] ); if (minBucketA !== minBucketB) { return minBucketA - minBucketB; } const maxBucketA = Math.max(aa1, aa2, aa3); const maxBucketB = Math.max(bb1, bb2, bb3,); if (maxBucketA !== maxBucketB) { return maxBucketA - maxBucketB; } return 0; } function preSortIndices(indices, bitsPerBucket) { const seq = new Int32Array(indices.length / 3); for (let i = 0, len = seq.length; i < len; i++) { seq[i] = i; } bucketsForIndices = new Int32Array(indices.length); for (let i = 0, len = indices.length; i < len; i++) { bucketsForIndices[i] = indices[i] >> bitsPerBucket; } seq.sort(compareBuckets); const sortedIndices = new Int32Array(indices.length); for (let i = 0, len = seq.length; i < len; i++) { sortedIndices[i * 3 + 0] = indices[seq[i] * 3 + 0]; sortedIndices[i * 3 + 1] = indices[seq[i] * 3 + 1]; sortedIndices[i * 3 + 2] = indices[seq[i] * 3 + 2]; } return sortedIndices; } let compareEdgeIndices = null; function compareIndices(a, b) { let retVal = compareEdgeIndices[a * 2] - compareEdgeIndices[b * 2]; if (retVal !== 0) { return retVal; } return compareEdgeIndices[a * 2 + 1] - compareEdgeIndices[b * 2 + 1]; } function preSortEdgeIndices(edgeIndices) { if ((edgeIndices || []).length === 0) { return []; } let seq = new Int32Array(edgeIndices.length / 2); for (let i = 0, len = seq.length; i < len; i++) { seq[i] = i; } for (let i = 0, len = edgeIndices.length; i < len; i += 2) { if (edgeIndices[i] > edgeIndices[i + 1]) { let tmp = edgeIndices[i]; edgeIndices[i] = edgeIndices[i + 1]; edgeIndices[i + 1] = tmp; } } compareEdgeIndices = new Int32Array(edgeIndices); seq.sort(compareIndices); const sortedEdgeIndices = new Int32Array(edgeIndices.length); for (let i = 0, len = seq.length; i < len; i++) { sortedEdgeIndices[i * 2 + 0] = edgeIndices[seq[i] * 2 + 0]; sortedEdgeIndices[i * 2 + 1] = edgeIndices[seq[i] * 2 + 1]; } return sortedEdgeIndices; } /** * @param {{positionsCompressed: number[], indices: number[], edgeIndices: number[]}} mesh * @param {number} bitsPerBucket * @param {boolean} checkResult * * @returns {{positionsCompressed: number[], indices: number[], edgeIndices: number[]}[]} */ function rebucketPositions(mesh, bitsPerBucket, checkResult = false) { const positionsCompressed = (mesh.positionsCompressed || []); const indices = preSortIndices(mesh.indices || [], bitsPerBucket); const edgeIndices = preSortEdgeIndices(mesh.edgeIndices || []); function edgeSearch(el0, el1) { // Code adapted from https://stackoverflow.com/questions/22697936/binary-search-in-javascript if (el0 > el1) { let tmp = el0; el0 = el1; el1 = tmp; } function compare_fn(a, b) { if (a !== el0) { return el0 - a; } if (b !== el1) { return el1 - b; } return 0; } let m = 0; let n = (edgeIndices.length >> 1) - 1; while (m <= n) { const k = (n + m) >> 1; const cmp = compare_fn(edgeIndices[k * 2], edgeIndices[k * 2 + 1]); if (cmp > 0) { m = k + 1; } else if (cmp < 0) { n = k - 1; } else { return k; } } return -m - 1; } const alreadyOutputEdgeIndices = new Int32Array(edgeIndices.length / 2); alreadyOutputEdgeIndices.fill(0); const numPositions = positionsCompressed.length / 3; if (numPositions > ((1 << bitsPerBucket) * MAX_RE_BUCKET_FAN_OUT)) { return [mesh]; } const bucketIndicesRemap = new Int32Array(numPositions); bucketIndicesRemap.fill(-1); const buckets = []; function addEmptyBucket() { bucketIndicesRemap.fill(-1); const newBucket = { positionsCompressed: [], indices: [], edgeIndices: [], maxNumPositions: (1 << bitsPerBucket) - bitsPerBucket, numPositions: 0, bucketNumber: buckets.length, }; buckets.push(newBucket); return newBucket; } let currentBucket = addEmptyBucket(); for (let i = 0, len = indices.length; i < len; i += 3) { let additonalPositionsInBucket = 0; const ii0 = indices[i]; const ii1 = indices[i + 1]; const ii2 = indices[i + 2]; if (bucketIndicesRemap[ii0] === -1) { additonalPositionsInBucket++; } if (bucketIndicesRemap[ii1] === -1) { additonalPositionsInBucket++; } if (bucketIndicesRemap[ii2] === -1) { additonalPositionsInBucket++; } if ((additonalPositionsInBucket + currentBucket.numPositions) > currentBucket.maxNumPositions) { currentBucket = addEmptyBucket(); } if (currentBucket.bucketNumber > MAX_RE_BUCKET_FAN_OUT) { return [mesh]; } if (bucketIndicesRemap[ii0] === -1) { bucketIndicesRemap[ii0] = currentBucket.numPositions++; currentBucket.positionsCompressed.push(positionsCompressed[ii0 * 3]); currentBucket.positionsCompressed.push(positionsCompressed[ii0 * 3 + 1]); currentBucket.positionsCompressed.push(positionsCompressed[ii0 * 3 + 2]); } if (bucketIndicesRemap[ii1] === -1) { bucketIndicesRemap[ii1] = currentBucket.numPositions++; currentBucket.positionsCompressed.push(positionsCompressed[ii1 * 3]); currentBucket.positionsCompressed.push(positionsCompressed[ii1 * 3 + 1]); currentBucket.positionsCompressed.push(positionsCompressed[ii1 * 3 + 2]); } if (bucketIndicesRemap[ii2] === -1) { bucketIndicesRemap[ii2] = currentBucket.numPositions++; currentBucket.positionsCompressed.push(positionsCompressed[ii2 * 3]); currentBucket.positionsCompressed.push(positionsCompressed[ii2 * 3 + 1]); currentBucket.positionsCompressed.push(positionsCompressed[ii2 * 3 + 2]); } currentBucket.indices.push(bucketIndicesRemap[ii0]); currentBucket.indices.push(bucketIndicesRemap[ii1]); currentBucket.indices.push(bucketIndicesRemap[ii2]); // Check possible edge1 let edgeIndex; if ((edgeIndex = edgeSearch(ii0, ii1)) >= 0) { if (alreadyOutputEdgeIndices[edgeIndex] === 0) { alreadyOutputEdgeIndices[edgeIndex] = 1; currentBucket.edgeIndices.push(bucketIndicesRemap[edgeIndices[edgeIndex * 2]]); currentBucket.edgeIndices.push(bucketIndicesRemap[edgeIndices[edgeIndex * 2 + 1]]); } } if ((edgeIndex = edgeSearch(ii0, ii2)) >= 0) { if (alreadyOutputEdgeIndices[edgeIndex] === 0) { alreadyOutputEdgeIndices[edgeIndex] = 1; currentBucket.edgeIndices.push(bucketIndicesRemap[edgeIndices[edgeIndex * 2]]); currentBucket.edgeIndices.push(bucketIndicesRemap[edgeIndices[edgeIndex * 2 + 1]]); } } if ((edgeIndex = edgeSearch(ii1, ii2)) >= 0) { if (alreadyOutputEdgeIndices[edgeIndex] === 0) { alreadyOutputEdgeIndices[edgeIndex] = 1; currentBucket.edgeIndices.push(bucketIndicesRemap[edgeIndices[edgeIndex * 2]]); currentBucket.edgeIndices.push(bucketIndicesRemap[edgeIndices[edgeIndex * 2 + 1]]); } } } const prevBytesPerIndex = bitsPerBucket / 8 * 2; const newBytesPerIndex = bitsPerBucket / 8; const originalSize = positionsCompressed.length * 2 + (indices.length + edgeIndices.length) * prevBytesPerIndex; let newSize = 0; let newPositions = -positionsCompressed.length / 3; buckets.forEach(bucket => { newSize += bucket.positionsCompressed.length * 2 + (bucket.indices.length + bucket.edgeIndices.length) * newBytesPerIndex; newPositions += bucket.positionsCompressed.length / 3; }); if (newSize > originalSize) { return [mesh]; } if (checkResult) { doCheckResult(buckets, mesh); } return buckets; } function doCheckResult(buckets, mesh) { const meshDict = {}; const edgesDict = {}; let edgeIndicesCount = 0; buckets.forEach(bucket => { const indices = bucket.indices; const edgeIndices = bucket.edgeIndices; const positionsCompressed = bucket.positionsCompressed; for (let i = 0, len = indices.length; i < len; i += 3) { const key = positionsCompressed[indices[i] * 3] + "_" + positionsCompressed[indices[i] * 3 + 1] + "_" + positionsCompressed[indices[i] * 3 + 2] + "/" + positionsCompressed[indices[i + 1] * 3] + "_" + positionsCompressed[indices[i + 1] * 3 + 1] + "_" + positionsCompressed[indices[i + 1] * 3 + 2] + "/" + positionsCompressed[indices[i + 2] * 3] + "_" + positionsCompressed[indices[i + 2] * 3 + 1] + "_" + positionsCompressed[indices[i + 2] * 3 + 2]; meshDict[key] = true; } edgeIndicesCount += bucket.edgeIndices.length / 2; for (let i = 0, len = edgeIndices.length; i < len; i += 2) { const key = positionsCompressed[edgeIndices[i] * 3] + "_" + positionsCompressed[edgeIndices[i] * 3 + 1] + "_" + positionsCompressed[edgeIndices[i] * 3 + 2] + "/" + positionsCompressed[edgeIndices[i + 1] * 3] + "_" + positionsCompressed[edgeIndices[i + 1] * 3 + 1] + "_" + positionsCompressed[edgeIndices[i + 1] * 3 + 2] + "/"; edgesDict[key] = true; } }); { const indices = mesh.indices; mesh.edgeIndices; const positionsCompressed = mesh.positionsCompressed; for (let i = 0, len = indices.length; i < len; i += 3) { const key = positionsCompressed[indices[i] * 3] + "_" + positionsCompressed[indices[i] * 3 + 1] + "_" + positionsCompressed[indices[i] * 3 + 2] + "/" + positionsCompressed[indices[i + 1] * 3] + "_" + positionsCompressed[indices[i + 1] * 3 + 1] + "_" + positionsCompressed[indices[i + 1] * 3 + 2] + "/" + positionsCompressed[indices[i + 2] * 3] + "_" + positionsCompressed[indices[i + 2] * 3 + 1] + "_" + positionsCompressed[indices[i + 2] * 3 + 2]; if (!(key in meshDict)) { console.log("Not found " + key); throw "Ohhhh!"; } } // for (var i = 0, len = edgeIndices.length; i < len; i+=2) // { // var key = positionsCompressed[edgeIndices[i]*3] + "_" + positionsCompressed[edgeIndices[i]*3+1] + "_" + positionsCompressed[edgeIndices[i]*3+2] + "/" + // positionsCompressed[edgeIndices[i+1]*3] + "_" + positionsCompressed[edgeIndices[i+1]*3+1] + "_" + positionsCompressed[edgeIndices[i+1]*3+2] + "/"; // if (!(key in edgesDict)) { // var key2 = edgeIndices[i] + "_" + edgeIndices[i+1]; // console.log (" - Not found " + key); // console.log (" - Not found " + key2); // // throw "Ohhhh2!"; // } // } } } const tempFloatRGB = new Float32Array([0, 0, 0]); const tempIntRGB = new Uint16Array([0, 0, 0]); math.OBB3(); /** * An entity within a {@link SceneModel} * * * Created with {@link SceneModel#createEntity} * * Stored by ID in {@link SceneModel#entities} * * Has one or more {@link SceneModelMesh}es * * @implements {Entity} */ class SceneModelEntity { /** * @private */ constructor(model, isObject, id, meshes, flags, lodCullable) { this._isObject = isObject; /** * The {@link Scene} to which this SceneModelEntity belongs. */ this.scene = model.scene; /** * The {@link SceneModel} to which this SceneModelEntity belongs. */ this.model = model; /** * Identifies if it's a SceneModelEntity */ this.isSceneModelEntity = true; /** * The {@link SceneModelMesh}es belonging to this SceneModelEntity. * * * These are created with {@link SceneModel#createMesh} and registered in {@ilnk SceneModel#meshes} * * Each SceneModelMesh belongs to one SceneModelEntity */ this.meshes = meshes; this._numPrimitives = 0; this._capMaterial = null; for (let i = 0, len = this.meshes.length; i < len; i++) { // TODO: tidier way? Refactor? const mesh = this.meshes[i]; mesh.parent = this; mesh.entity = this; this._numPrimitives += mesh.numPrimitives; } /** * The unique ID of this SceneModelEntity. */ this.id = id; /** * The original system ID of this SceneModelEntity. */ this.originalSystemId = math.unglobalizeObjectId(model.id, id); this._flags = flags; this._aabb = math.AABB3(); this._aabbDirty = true; this._offset = math.vec3(); this._colorizeUpdated = false; this._opacityUpdated = false; this._lodCullable = (!!lodCullable); this._culled = false; this._culledVFC = false; this._culledLOD = false; if (this._isObject) { model.scene._registerObject(this); } } _transformDirty() { this._aabbDirty = true; this.model._transformDirty(); } _sceneModelDirty() { // Called by SceneModel when SceneModel's matrix is updated this._aabbDirty = true; for (let i = 0, len = this.meshes.length; i < len; i++) { this.meshes[i]._sceneModelDirty(); } } /** * World-space 3D axis-aligned bounding box (AABB) of this SceneModelEntity. * * Represented by a six-element Float64Array containing the min/max extents of the * axis-aligned volume, ie. ````[xmin, ymin, zmin, xmax, ymax, zmax]````. * * @type {Float64Array} */ get aabb() { if (this._aabbDirty) { math.collapseAABB3(this._aabb); for (let i = 0, len = this.meshes.length; i < len; i++) { math.expandAABB3(this._aabb, this.meshes[i].aabb); } this._aabbDirty = false; } // if (this._aabbDirty) { // math.AABB3ToOBB3(this._aabb, tempOBB3a); // math.transformOBB3(this.model.matrix, tempOBB3a); // math.OBB3ToAABB3(tempOBB3a, this._worldAABB); // this._worldAABB[0] += this._offset[0]; // this._worldAABB[1] += this._offset[1]; // this._worldAABB[2] += this._offset[2]; // this._worldAABB[3] += this._offset[0]; // this._worldAABB[4] += this._offset[1]; // this._worldAABB[5] += this._offset[2]; // this._aabbDirty = false; // } return this._aabb; } get isEntity() { return true; } /** * Returns false to indicate that this Entity subtype is not a model. * @returns {boolean} */ get isModel() { return false; } /** * Returns ````true```` if this SceneModelEntity represents an object. * * When this is ````true````, the SceneModelEntity will be registered by {@link SceneModelEntity#id} * in {@link Scene#objects} and may also have a corresponding {@link MetaObject}. * * @type {Boolean} */ get isObject() { return this._isObject; } get numPrimitives() { return this._numPrimitives; } /** * The approximate number of triangles in this SceneModelEntity. * * @type {Number} */ get numTriangles() { return this._numPrimitives; } /** * Gets if this SceneModelEntity is visible. * * Only rendered when {@link SceneModelEntity#visible} is ````true```` * and {@link SceneModelEntity#culled} is ````false````. * * When {@link SceneModelEntity#isObject} and {@link SceneModelEntity#visible} are * both ````true```` the SceneModelEntity will be registered * by {@link SceneModelEntity#id} in {@link Scene#visibleObjects}. * * @type {Boolean} */ get visible() { return this._getFlag(ENTITY_FLAGS.VISIBLE); } /** * Sets if this SceneModelEntity is visible. * * Only rendered when {@link SceneModelEntity#visible} is ````true```` and {@link SceneModelEntity#culled} is ````false````. * * When {@link SceneModelEntity#isObject} and {@link SceneModelEntity#visible} are * both ````true```` the SceneModelEntity will be * registered by {@link SceneModelEntity#id} in {@link Scene#visibleObjects}. * * @type {Boolean} */ set visible(visible) { if (!!(this._flags & ENTITY_FLAGS.VISIBLE) === visible) { return; // Redundant update } if (visible) { this._flags = this._flags | ENTITY_FLAGS.VISIBLE; } else { this._flags = this._flags & ~ENTITY_FLAGS.VISIBLE; } for (let i = 0, len = this.meshes.length; i < len; i++) { this.meshes[i]._setVisible(this._flags); } if (this._isObject) { this.model.scene._objectVisibilityUpdated(this); } this.model.glRedraw(); } /** * Gets if this SceneModelEntity is highlighted. * * When {@link SceneModelEntity#isObject} and {@link SceneModelEntity#highlighted} are both ````true```` the SceneModelEntity will be * registered by {@link SceneModelEntity#id} in {@link Scene#highlightedObjects}. * * @type {Boolean} */ get highlighted() { return this._getFlag(ENTITY_FLAGS.HIGHLIGHTED); } /** * Sets if this SceneModelEntity is highlighted. * * When {@link SceneModelEntity#isObject} and {@link SceneModelEntity#highlighted} are both ````true```` the SceneModelEntity will be * registered by {@link SceneModelEntity#id} in {@link Scene#highlightedObjects}. * * @type {Boolean} */ set highlighted(highlighted) { if (!!(this._flags & ENTITY_FLAGS.HIGHLIGHTED) === highlighted) { return; // Redundant update } if (highlighted) { this._flags = this._flags | ENTITY_FLAGS.HIGHLIGHTED; } else { this._flags = this._flags & ~ENTITY_FLAGS.HIGHLIGHTED; } for (var i = 0, len = this.meshes.length; i < len; i++) { this.meshes[i]._setHighlighted(this._flags); } if (this._isObject) { this.model.scene._objectHighlightedUpdated(this); } this.model.glRedraw(); } /** * Gets if this SceneModelEntity is xrayed. * * When {@link SceneModelEntity#isObject} and {@link SceneModelEntity#xrayed} are both ````true``` the SceneModelEntity will be * registered by {@link SceneModelEntity#id} in {@link Scene#xrayedObjects}. * * @type {Boolean} */ get xrayed() { return this._getFlag(ENTITY_FLAGS.XRAYED); } /** * Sets if this SceneModelEntity is xrayed. * * When {@link SceneModelEntity#isObject} and {@link SceneModelEntity#xrayed} are both ````true``` the SceneModelEntity will be * registered by {@link SceneModelEntity#id} in {@link Scene#xrayedObjects}. * * @type {Boolean} */ set xrayed(xrayed) { if (!!(this._flags & ENTITY_FLAGS.XRAYED) === xrayed) { return; // Redundant update } if (xrayed) { this._flags = this._flags | ENTITY_FLAGS.XRAYED; } else { this._flags = this._flags & ~ENTITY_FLAGS.XRAYED; } for (let i = 0, len = this.meshes.length; i < len; i++) { this.meshes[i]._setXRayed(this._flags); } if (this._isObject) { this.model.scene._objectXRayedUpdated(this); } this.model.glRedraw(); } /** * Gets if this SceneModelEntity is selected. * * When {@link SceneModelEntity#isObject} and {@link SceneModelEntity#selected} are both ````true``` the SceneModelEntity will be * registered by {@link SceneModelEntity#id} in {@link Scene#selectedObjects}. * * @type {Boolean} */ get selected() { return this._getFlag(ENTITY_FLAGS.SELECTED); } /** * Gets if this SceneModelEntity is selected. * * When {@link SceneModelEntity#isObject} and {@link SceneModelEntity#selected} are both ````true``` the SceneModelEntity will be * registered by {@link SceneModelEntity#id} in {@link Scene#selectedObjects}. * * @type {Boolean} */ set selected(selected) { if (!!(this._flags & ENTITY_FLAGS.SELECTED) === selected) { return; // Redundant update } if (selected) { this._flags = this._flags | ENTITY_FLAGS.SELECTED; } else { this._flags = this._flags & ~ENTITY_FLAGS.SELECTED; } for (let i = 0, len = this.meshes.length; i < len; i++) { this.meshes[i]._setSelected(this._flags); } if (this._isObject) { this.model.scene._objectSelectedUpdated(this); } this.model.glRedraw(); } /** * Gets if this SceneModelEntity's edges are enhanced. * * @type {Boolean} */ get edges() { return this._getFlag(ENTITY_FLAGS.EDGES); } /** * Sets if this SceneModelEntity's edges are enhanced. * * @type {Boolean} */ set edges(edges) { if (!!(this._flags & ENTITY_FLAGS.EDGES) === edges) { return; // Redundant update } if (edges) { this._flags = this._flags | ENTITY_FLAGS.EDGES; } else { this._flags = this._flags & ~ENTITY_FLAGS.EDGES; } for (var i = 0, len = this.meshes.length; i < len; i++) { this.meshes[i]._setEdges(this._flags); } this.model.glRedraw(); } get culledVFC() { return !!(this._culledVFC); } set culledVFC(culled) { this._culledVFC = culled; this._setCulled(); } get culledLOD() { return !!(this._culledLOD); } set culledLOD(culled) { this._culledLOD = culled; this._setCulled(); } /** * Gets if this SceneModelEntity is culled. * * Only rendered when {@link SceneModelEntity#visible} is ````true```` and {@link SceneModelEntity#culled} is ````false````. * * @type {Boolean} */ get culled() { return !!(this._culled); // return this._getFlag(ENTITY_FLAGS.CULLED); } /** * Sets if this SceneModelEntity is culled. * * Only rendered when {@link SceneModelEntity#visible} is ````true```` and {@link SceneModelEntity#culled} is ````false````. * * @type {Boolean} */ set culled(culled) { this._culled = culled; this._setCulled(); } _setCulled() { let culled = !!(this._culled) || !!(this._culledLOD && this._lodCullable) || !!(this._culledVFC); if (!!(this._flags & ENTITY_FLAGS.CULLED) === culled) { return; // Redundant update } if (culled) { this._flags = this._flags | ENTITY_FLAGS.CULLED; } else { this._flags = this._flags & ~ENTITY_FLAGS.CULLED; } for (var i = 0, len = this.meshes.length; i < len; i++) { this.meshes[i]._setCulled(this._flags); } this.model.glRedraw(); } /** * Gets if this SceneModelEntity is clippable. * * Clipping is done by the {@link SectionPlane}s in {@link Scene#sectionPlanes}. * * @type {Boolean} */ get clippable() { return this._getFlag(ENTITY_FLAGS.CLIPPABLE); } /** * Sets if this SceneModelEntity is clippable. * * Clipping is done by the {@link SectionPlane}s in {@link Scene#sectionPlanes}. * * @type {Boolean} */ set clippable(clippable) { if ((!!(this._flags & ENTITY_FLAGS.CLIPPABLE)) === clippable) { return; // Redundant update } if (clippable) { this._flags = this._flags | ENTITY_FLAGS.CLIPPABLE; } else { this._flags = this._flags & ~ENTITY_FLAGS.CLIPPABLE; } for (var i = 0, len = this.meshes.length; i < len; i++) { this.meshes[i]._setClippable(this._flags); } this.model.glRedraw(); } /** * Gets if this SceneModelEntity is included in boundary calculations. * * @type {Boolean} */ get collidable() { return this._getFlag(ENTITY_FLAGS.COLLIDABLE); } /** * Sets if this SceneModelEntity is included in boundary calculations. * * @type {Boolean} */ set collidable(collidable) { if (!!(this._flags & ENTITY_FLAGS.COLLIDABLE) === collidable) { return; // Redundant update } if (collidable) { this._flags = this._flags | ENTITY_FLAGS.COLLIDABLE; } else { this._flags = this._flags & ~ENTITY_FLAGS.COLLIDABLE; } for (var i = 0, len = this.meshes.length; i < len; i++) { this.meshes[i]._setCollidable(this._flags); } } /** * Gets if this SceneModelEntity is pickable. * * Picking is done via calls to {@link Scene#pick}. * * @type {Boolean} */ get pickable() { return this._getFlag(ENTITY_FLAGS.PICKABLE); } /** * Sets if this SceneModelEntity is pickable. * * Picking is done via calls to {@link Scene#pick}. * * @type {Boolean} */ set pickable(pickable) { if (!!(this._flags & ENTITY_FLAGS.PICKABLE) === pickable) { return; // Redundant update } if (pickable) { this._flags = this._flags | ENTITY_FLAGS.PICKABLE; } else { this._flags = this._flags & ~ENTITY_FLAGS.PICKABLE; } for (var i = 0, len = this.meshes.length; i < len; i++) { this.meshes[i]._setPickable(this._flags); } } /** * Gets the SceneModelEntity's RGB colorize color, multiplies by the SceneModelEntity's rendered fragment colors. * * Each element of the color is in range ````[0..1]````. * * @type {Number[]} */ get colorize() { // [0..1, 0..1, 0..1] if (this.meshes.length === 0) { return null; } const colorize = this.meshes[0]._colorize; tempFloatRGB[0] = colorize[0] / 255.0; // Unquantize tempFloatRGB[1] = colorize[1] / 255.0; tempFloatRGB[2] = colorize[2] / 255.0; return tempFloatRGB; } /** * Sets the SceneModelEntity's RGB colorize color, multiplies by the SceneModelEntity's rendered fragment colors. * * Each element of the color is in range ````[0..1]````. * * @type {Number[]} */ set colorize(color) { // [0..1, 0..1, 0..1] if (color) { tempIntRGB[0] = Math.floor(color[0] * 255.0); // Quantize tempIntRGB[1] = Math.floor(color[1] * 255.0); tempIntRGB[2] = Math.floor(color[2] * 255.0); for (let i = 0, len = this.meshes.length; i < len; i++) { this.meshes[i]._setColorize(tempIntRGB); } } else { for (let i = 0, len = this.meshes.length; i < len; i++) { this.meshes[i]._setColorize(null); } } if (this._isObject) { const colorized = (!!color); this.scene._objectColorizeUpdated(this, colorized); this._colorizeUpdated = colorized; } this.model.glRedraw(); } /** * Gets the SceneModelEntity's opacity factor. * * This is a factor in range ````[0..1]```` which multiplies by the rendered fragment alphas. * * @type {Number} */ get opacity() { if (this.meshes.length > 0) { return (this.meshes[0]._colorize[3] / 255.0); } else { return 1.0; } } /** * Sets the SceneModelEntity's opacity factor. * * This is a factor in range ````[0..1]```` which multiplies by the rendered fragment alphas. * * @type {Number} */ set opacity(opacity) { if (this.meshes.length === 0) { return; } const opacityUpdated = (opacity !== null && opacity !== undefined); const lastOpacityQuantized = this.meshes[0]._colorize[3]; let opacityQuantized = 255; if (opacityUpdated) { if (opacity < 0) { opacity = 0; } else if (opacity > 1) { opacity = 1; } opacityQuantized = Math.floor(opacity * 255.0); // Quantize if (lastOpacityQuantized === opacityQuantized) { return; } } else { opacityQuantized = 255.0; if (lastOpacityQuantized === opacityQuantized) { return; } } for (let i = 0, len = this.meshes.length; i < len; i++) { this.meshes[i]._setOpacity(opacityQuantized, this._flags); } if (this._isObject) { this.scene._objectOpacityUpdated(this, opacityUpdated); this._opacityUpdated = opacityUpdated; } this.model.glRedraw(); } /** * Gets the SceneModelEntity's 3D World-space offset. * * Default value is ````[0,0,0]````. * * @type {Number[]} */ get offset() { return this._offset; } /** * Sets the SceneModelEntity's 3D World-space offset. * * Default value is ````[0,0,0]````. * * @type {Number[]} */ set offset(offset) { if (offset) { this._offset[0] = offset[0]; this._offset[1] = offset[1]; this._offset[2] = offset[2]; } else { this._offset[0] = 0; this._offset[1] = 0; this._offset[2] = 0; } for (let i = 0, len = this.meshes.length; i < len; i++) { this.meshes[i]._setOffset(this._offset); } this._aabbDirty = true; this.model._aabbDirty = true; this.scene._aabbDirty = true; this.scene._objectOffsetUpdated(this, offset); this.model.glRedraw(); } get saoEnabled() { return this.model.saoEnabled; } /** * Sets the SceneModelEntity's capMaterial that will be used on the caps generated when this entity is sliced * * Default value is ````null````. * * @type {Material} */ set capMaterial(value) { if(!this.scene.readableGeometryEnabled) return; this._capMaterial = value instanceof Material ? value : null; this.scene._capMaterialUpdated(this.id, this.model.id); } /** * Gets the SceneModelEntity's capMaterial. * * Default value is ````null````. * * @type {Material} */ get capMaterial() { return this._capMaterial; } getEachVertex(callback) { for (let i = 0, len = this.meshes.length; i < len; i++) { this.meshes[i].getEachVertex(callback); } } getEachIndex(callback) { for (let i = 0, len = this.meshes.length; i < len; i++) { this.meshes[i].getEachIndex(callback); } } getGeometryData() { let positionsBase = 0; const positions = []; const indices = []; for (let i = 0, len = this.meshes.length; i < len; i++) { const mesh = this.meshes[i]; if (mesh.layer.readGeometryData) { const meshGeom = mesh.layer.readGeometryData(mesh.portionId); for (let j = 0, len = meshGeom.indices.length; j < len; j++) { indices.push(meshGeom.indices[j]+positionsBase); } for (let j = 0, len = meshGeom.positions.length; j < len; j++) { positions.push(meshGeom.positions[j]); } positionsBase += meshGeom.positions.length / 3; } } return { indices, positions }; } /** * Returns the volume of this SceneModelEntity. * * Only works when {@link Scene.readableGeometryEnabled | Scene.readableGeometryEnabled} is `true` and the * SceneModelEntity contains solid triangle meshes; returns `0` otherwise. * * @returns {number} */ get volume() { let volume = 0; for (let i = 0, len = this.meshes.length; i < len; i++) { const meshVolume = this.meshes[i].volume; if (meshVolume < 0) { return -1; } volume += meshVolume; } return volume; } /** * Returns the surface area of this SceneModelEntity. * * Only works when {@link Scene.readableGeometryEnabled | Scene.readableGeometryEnabled} is `true` and the * SceneModelEntity contains triangle meshes; returns `0` otherwise. * * @returns {number} */ get surfaceArea() { let surfaceArea = 0; for (let i = 0, len = this.meshes.length; i < len; i++) { const meshSurfaceArea = this.meshes[i].surfaceArea; if (meshSurfaceArea >= 0) { surfaceArea += meshSurfaceArea; } } return surfaceArea > 0 ? surfaceArea : -1; } _getFlag(flag) { return !!(this._flags & flag); } _finalize() { const scene = this.model.scene; if (this._isObject) { if (this.visible) { scene._objectVisibilityUpdated(this); } if (this.highlighted) { scene._objectHighlightedUpdated(this); } if (this.xrayed) { scene._objectXRayedUpdated(this); } if (this.selected) { scene._objectSelectedUpdated(this); } } for (let i = 0, len = this.meshes.length; i < len; i++) { this.meshes[i]._finalize(this._flags); } } _finalize2() { for (let i = 0, len = this.meshes.length; i < len; i++) { this.meshes[i]._finalize2(); } } _destroy() { const scene = this.model.scene; if (this._isObject) { scene._deregisterObject(this); if (this.visible) { scene._deRegisterVisibleObject(this); } if (this.xrayed) { scene._deRegisterXRayedObject(this); } if (this.selected) { scene._deRegisterSelectedObject(this); } if (this.highlighted) { scene._deRegisterHighlightedObject(this); } if (this._colorizeUpdated) { this.scene._deRegisterColorizedObject(this); } if (this._opacityUpdated) { this.scene._deRegisterOpacityObject(this); } if (this._offset && (this._offset[0] !== 0 || this._offset[1] !== 0 || this._offset[2] !== 0)) { this.scene._deRegisterOffsetObject(this); } } for (let i = 0, len = this.meshes.length; i < len; i++) { this.meshes[i]._destroy(); } scene._aabbDirty = true; } } const angleAxis = math.vec4(4); const q1 = math.vec4(); const q2 = math.vec4(); const xAxis = math.vec3([1, 0, 0]); const yAxis = math.vec3([0, 1, 0]); const zAxis = math.vec3([0, 0, 1]); math.vec3(3); math.vec3(3); const identityMat = math.identityMat4(); /** * A dynamically-updatable transform within a {@link SceneModel}. * * * Can be composed into hierarchies * * Shared by multiple {@link SceneModelMesh}es * * Created with {@link SceneModel#createTransform} * * Stored by ID in {@link SceneModel#transforms} * * Referenced by {@link SceneModelMesh#transform} */ class SceneModelTransform { /** * @private */ constructor(cfg) { this._model = cfg.model; /** * Unique ID of this SceneModelTransform. * * The SceneModelTransform is registered against this ID in {@link SceneModel#transforms}. */ this.id = cfg.id; this._parentTransform = cfg.parent; this._childTransforms = []; this._meshes = []; this._scale = new Float32Array([1,1,1]); this._quaternion = math.identityQuaternion(new Float32Array(4)); this._rotation = new Float32Array(3); this._position = new Float32Array(3); this._localMatrix = math.identityMat4(new Float32Array(16)); this._worldMatrix = math.identityMat4(new Float32Array(16)); this._localMatrixDirty = true; this._worldMatrixDirty = true; if (cfg.matrix) { this.matrix = cfg.matrix; } else { this.scale = cfg.scale; this.position = cfg.position; if (cfg.quaternion) ; else { this.rotation = cfg.rotation; } } if (cfg.parent) { cfg.parent._addChildTransform(this); } } _addChildTransform(childTransform) { this._childTransforms.push(childTransform); childTransform._parentTransform = this; childTransform._transformDirty(); childTransform._setSubtreeAABBsDirty(this); } _addMesh(mesh) { this._meshes.push(mesh); mesh.transform = this; // childTransform._setWorldMatrixDirty(); // childTransform._setAABBDirty(); } /** * The optional parent SceneModelTransform. * * @type {SceneModelTransform} */ get parentTransform() { return this._parentTransform; } /** * The {@link SceneModelMesh}es transformed by this SceneModelTransform. * * @returns {[]} */ get meshes() { return this._meshes; } /** * Sets the SceneModelTransform's local translation. * * Default value is ````[0,0,0]````. * * @type {Number[]} */ set position(value) { this._position.set(value || [0, 0, 0]); this._setLocalMatrixDirty(); this._model.glRedraw(); } /** * Gets the SceneModelTransform's translation. * * Default value is ````[0,0,0]````. * * @type {Number[]} */ get position() { return this._position; } /** * Sets the SceneModelTransform's rotation, as Euler angles given in degrees, for each of the X, Y and Z axis. * * Default value is ````[0,0,0]````. * * @type {Number[]} */ set rotation(value) { this._rotation.set(value || [0, 0, 0]); math.eulerToQuaternion(this._rotation, "XYZ", this._quaternion); this._setLocalMatrixDirty(); this._model.glRedraw(); } /** * Gets the SceneModelTransform's rotation, as Euler angles given in degrees, for each of the X, Y and Z axis. * * Default value is ````[0,0,0]````. * * @type {Number[]} */ get rotation() { return this._rotation; } /** * Sets the SceneModelTransform's rotation quaternion. * * Default value is ````[0,0,0,1]````. * * @type {Number[]} */ set quaternion(value) { this._quaternion.set(value || [0, 0, 0, 1]); math.quaternionToEuler(this._quaternion, "XYZ", this._rotation); this._setLocalMatrixDirty(); this._model.glRedraw(); } /** * Gets the SceneModelTransform's rotation quaternion. * * Default value is ````[0,0,0,1]````. * * @type {Number[]} */ get quaternion() { return this._quaternion; } /** * Sets the SceneModelTransform's scale. * * Default value is ````[1,1,1]````. * * @type {Number[]} */ set scale(value) { this._scale.set(value || [1, 1, 1]); this._setLocalMatrixDirty(); this._model.glRedraw(); } /** * Gets the SceneModelTransform's scale. * * Default value is ````[1,1,1]````. * * @type {Number[]} */ get scale() { return this._scale; } /** * Sets the SceneModelTransform's transform matrix. * * Default value is ````[1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]````. * * @type {Number[]} */ set matrix(value) { if (!this._localMatrix) { this._localMatrix = math.identityMat4(); } this._localMatrix.set(value || identityMat); math.decomposeMat4(this._localMatrix, this._position, this._quaternion, this._scale); this._localMatrixDirty = false; this._transformDirty(); this._model.glRedraw(); } /** * Gets the SceneModelTransform's transform matrix. * * Default value is ````[1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]````. * * @type {Number[]} */ get matrix() { if (this._localMatrixDirty) { if (!this._localMatrix) { this._localMatrix = math.identityMat4(); } math.composeMat4(this._position, this._quaternion, this._scale, this._localMatrix); this._localMatrixDirty = false; } return this._localMatrix; } /** * Gets the SceneModelTransform's World matrix. * * @property worldMatrix * @type {Number[]} */ get worldMatrix() { if (this._worldMatrixDirty) { this._buildWorldMatrix(); } return this._worldMatrix; } /** * Rotates the SceneModelTransform about the given axis by the given increment. * * @param {Number[]} axis Local axis about which to rotate. * @param {Number} angle Angle increment in degrees. */ rotate(axis, angle) { angleAxis[0] = axis[0]; angleAxis[1] = axis[1]; angleAxis[2] = axis[2]; angleAxis[3] = angle * math.DEGTORAD; math.angleAxisToQuaternion(angleAxis, q1); math.mulQuaternions(this.quaternion, q1, q2); this.quaternion = q2; this._setLocalMatrixDirty(); this._model.glRedraw(); return this; } /** * Rotates the SceneModelTransform about the given World-space axis by the given increment. * * @param {Number[]} axis Local axis about which to rotate. * @param {Number} angle Angle increment in degrees. */ rotateOnWorldAxis(axis, angle) { angleAxis[0] = axis[0]; angleAxis[1] = axis[1]; angleAxis[2] = axis[2]; angleAxis[3] = angle * math.DEGTORAD; math.angleAxisToQuaternion(angleAxis, q1); math.mulQuaternions(q1, this.quaternion, q1); //this.quaternion.premultiply(q1); return this; } /** * Rotates the SceneModelTransform about the local X-axis by the given increment. * * @param {Number} angle Angle increment in degrees. */ rotateX(angle) { return this.rotate(xAxis, angle); } /** * Rotates the SceneModelTransform about the local Y-axis by the given increment. * * @param {Number} angle Angle increment in degrees. */ rotateY(angle) { return this.rotate(yAxis, angle); } /** * Rotates the SceneModelTransform about the local Z-axis by the given increment. * * @param {Number} angle Angle increment in degrees. */ rotateZ(angle) { return this.rotate(zAxis, angle); } /** * Translates the SceneModelTransform along the local axis by the given increment. * * @param {Number[]} axis Normalized local space 3D vector along which to translate. * @param {Number} distance Distance to translate along the vector. */ translate(axis) { this._position[0] += axis[0]; this._position[1] += axis[1]; this._position[2] += axis[2]; this._setLocalMatrixDirty(); this._model.glRedraw(); return this; } /** * Translates the SceneModelTransform along the local X-axis by the given increment. * * @param {Number} distance Distance to translate along the X-axis. */ translateX(distance) { this._position[0] += distance; this._setLocalMatrixDirty(); this._model.glRedraw(); return this; } /** * Translates the SceneModelTransform along the local Y-axis by the given increment. * * @param {Number} distance Distance to translate along the Y-axis. */ translateY(distance) { this._position[1] += distance; this._setLocalMatrixDirty(); this._model.glRedraw(); return this; } /** * Translates the SceneModelTransform along the local Z-axis by the given increment. * * @param {Number} distance Distance to translate along the Z-axis. */ translateZ(distance) { this._position[2] += distance; this._setLocalMatrixDirty(); this._model.glRedraw(); return this; } _setLocalMatrixDirty() { this._localMatrixDirty = true; this._transformDirty(); } _transformDirty() { this._worldMatrixDirty = true; for (let i = 0, len = this._childTransforms.length; i < len; i++) { const childTransform = this._childTransforms[i]; childTransform._transformDirty(); if (childTransform._meshes && childTransform._meshes.length > 0) { const meshes = childTransform._meshes; for (let j =0, lenj = meshes.length; j < lenj; j++) { meshes[j]._transformDirty(); } } } if (this._meshes && this._meshes.length > 0) { const meshes = this._meshes; for (let j =0, lenj = meshes.length; j < lenj; j++) { meshes[j]._transformDirty(); } } } _buildWorldMatrix() { const localMatrix = this.matrix; if (!this._parentTransform) { for (let i = 0, len = localMatrix.length; i < len; i++) { this._worldMatrix[i] = localMatrix[i]; } } else { math.mulMat4(this._parentTransform.worldMatrix, localMatrix, this._worldMatrix); } this._worldMatrixDirty = false; } _setSubtreeAABBsDirty(sceneTransform) { sceneTransform._aabbDirty = true; if (sceneTransform._childTransforms) { for (let i = 0, len = sceneTransform._childTransforms.length; i < len; i++) { this._setSubtreeAABBsDirty(sceneTransform._childTransforms[i]); } } } } const tempVec3a$d = math.vec3(); const tempOBB3 = math.OBB3(); const tempQuaternion = math.vec4(); const DEFAULT_SCALE = math.vec3([1, 1, 1]); const DEFAULT_POSITION = math.vec3([0, 0, 0]); math.vec3([0, 0, 0]); const DEFAULT_QUATERNION = math.identityQuaternion(); const DEFAULT_MATRIX = math.identityMat4(); const DEFAULT_COLOR_TEXTURE_ID = "defaultColorTexture"; const DEFAULT_METAL_ROUGH_TEXTURE_ID = "defaultMetalRoughTexture"; const DEFAULT_NORMALS_TEXTURE_ID = "defaultNormalsTexture"; const DEFAULT_EMISSIVE_TEXTURE_ID = "defaultEmissiveTexture"; const DEFAULT_OCCLUSION_TEXTURE_ID = "defaultOcclusionTexture"; const DEFAULT_TEXTURE_SET_ID = "defaultTextureSet"; const defaultCompressedColor = new Uint8Array([255, 255, 255]); const VBO_INSTANCED = 0; const VBO_BATCHED = 1; const DTX = 2; /** * @desc A high-performance model representation for efficient rendering and low memory usage. * * # Examples * * Internally, SceneModel uses a combination of several different techniques to render and represent * the different parts of a typical model. Each of the live examples at these links is designed to "unit test" one of these * techniques, in isolation. If some bug occurs in SceneModel, we use these tests to debug, but they also * serve to demonstrate how to use the capabilities of SceneModel programmatically. * * * [Loading building models into SceneModels](/examples/buildings) * * [Loading city models into SceneModels](/examples/cities) * * [Loading LiDAR scans into SceneModels](/examples/lidar) * * [Loading CAD models into SceneModels](/examples/cad) * * [SceneModel feature tests](/examples/scenemodel) * * # Overview * * While xeokit's standard [scene graph](https://github.com/xeokit/xeokit-sdk/wiki/Scene-Graphs) is great for gizmos and medium-sized models, it doesn't scale up to millions of objects in terms of memory and rendering efficiency. * * For huge models, we have the ````SceneModel```` representation, which is optimized to pack large amounts of geometry into memory and render it efficiently using WebGL. * * ````SceneModel```` is the default model representation loaded by (at least) {@link GLTFLoaderPlugin}, {@link XKTLoaderPlugin} and {@link WebIFCLoaderPlugin}. * * In this tutorial you'll learn how to use ````SceneModel```` to create high-detail content programmatically. Ordinarily you'd be learning about ````SceneModel```` if you were writing your own model loader plugins. * * # Contents * * - [SceneModel](#DataTextureSceneModel) * - [GPU-Resident Geometry](#gpu-resident-geometry) * - [Picking](#picking) * - [Example 1: Geometry Instancing](#example-1--geometry-instancing) * - [Finalizing a SceneModel](#finalizing-a-DataTextureSceneModel) * - [Finding Entities](#finding-entities) * - [Example 2: Geometry Batching](#example-2--geometry-batching) * - [Classifying with Metadata](#classifying-with-metadata) * - [Querying Metadata](#querying-metadata) * - [Metadata Structure](#metadata-structure) * - [RTC Coordinates](#rtc-coordinates-for-double-precision) * - [Example 3: RTC Coordinates with Geometry Instancing](#example-2--rtc-coordinates-with-geometry-instancing) * - [Example 4: RTC Coordinates with Geometry Batching](#example-2--rtc-coordinates-with-geometry-batching) * * ## SceneModel * * ````SceneModel```` uses two rendering techniques internally: * * 1. ***Geometry batching*** for unique geometries, combining those into a single WebGL geometry buffer, to render in one draw call, and * 2. ***geometry instancing*** for geometries that are shared by multiple meshes, rendering all instances of each shared geometry in one draw call. * *
* These techniques come with certain limitations: * * * Non-realistic rendering - while scene graphs can use xeokit's full set of material workflows, ````SceneModel```` uses simple Lambertian shading without textures. * * Static transforms - transforms within a ````SceneModel```` are static and cannot be dynamically translated, rotated and scaled the way {@link Node}s and {@link Mesh}es in scene graphs can. * * Immutable model representation - while scene graph {@link Node}s and * {@link Mesh}es can be dynamically plugged together, ````SceneModel```` is immutable, * since it packs its geometries into buffers and instanced arrays. * * ````SceneModel````'s API allows us to exploit batching and instancing, while exposing its elements as * abstract {@link Entity} types. * * {@link Entity} is the abstract base class for * the various xeokit components that represent models, objects, or anonymous visible elements. An Entity has a unique ID and can be * individually shown, hidden, selected, highlighted, ghosted, culled, picked and clipped, and has its own World-space boundary. * * * A ````SceneModel```` is an {@link Entity} that represents a model. * * A ````SceneModel```` represents each of its objects with an {@link Entity}. * * Each {@link Entity} has one or more meshes that define its shape. * * Each mesh has either its own unique geometry, or shares a geometry with other meshes. * * ## GPU-Resident Geometry * * For a low memory footprint, ````SceneModel```` stores its geometries in GPU memory only, compressed (quantized) as integers. Unfortunately, GPU-resident geometry is * not readable by JavaScript. * * * ## Example 1: Geometry Instancing * * In the example below, we'll use a ````SceneModel```` * to build a simple table model using geometry instancing. * * We'll start by adding a reusable box-shaped geometry to our ````SceneModel````. * * Then, for each object in our model we'll add an {@link Entity} * that has a mesh that instances our box geometry, transforming and coloring the instance. * * [![](http://xeokit.io/img/docs/sceneGraph.png)](https://xeokit.github.io/xeokit-sdk/examples/index.html#sceneRepresentation_SceneModel_instancing) * * ````javascript * import {Viewer, SceneModel} from "xeokit-sdk.es.js"; * * const viewer = new Viewer({ * canvasId: "myCanvas", * transparent: true * }); * * viewer.scene.camera.eye = [-21.80, 4.01, 6.56]; * viewer.scene.camera.look = [0, -5.75, 0]; * viewer.scene.camera.up = [0.37, 0.91, -0.11]; * * // Build a SceneModel representing a table * // with four legs, using geometry instancing * * const sceneModel = new SceneModel(viewer.scene, { * id: "table", * isModel: true, // <--- Registers SceneModel in viewer.scene.models * position: [0, 0, 0], * scale: [1, 1, 1], * rotation: [0, 0, 0] * }); * * // Create a reusable geometry within the SceneModel * // We'll instance this geometry by five meshes * * sceneModel.createGeometry({ * * id: "myBoxGeometry", * * // The primitive type - allowed values are "points", "lines" and "triangles". * // See the OpenGL/WebGL specification docs * // for how the coordinate arrays are supposed to be laid out. * primitive: "triangles", * * // The vertices - eight for our cube, each * // one spanning three array elements for X,Y and Z * positions: [ * 1, 1, 1, -1, 1, 1, -1, -1, 1, 1, -1, 1, // v0-v1-v2-v3 front * 1, 1, 1, 1, -1, 1, 1, -1, -1, 1, 1, -1, // v0-v3-v4-v1 right * 1, 1, 1, 1, 1, -1, -1, 1, -1, -1, 1, 1, // v0-v1-v6-v1 top * -1, 1, 1, -1, 1, -1, -1, -1, -1, -1, -1, 1, // v1-v6-v7-v2 left * -1, -1, -1, 1, -1, -1, 1, -1, 1, -1, -1, 1, // v7-v4-v3-v2 bottom * 1, -1, -1, -1, -1, -1, -1, 1, -1, 1, 1, -1 // v4-v7-v6-v1 back * ], * * // Normal vectors, one for each vertex * normals: [ * 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, // v0-v1-v2-v3 front * 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, // v0-v3-v4-v5 right * 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, // v0-v5-v6-v1 top * -1, 0, 0, -1, 0, 0, -1, 0, 0, -1, 0, 0, // v1-v6-v7-v2 left * 0, -1, 0, 0, -1, 0, 0, -1, 0, 0, -1, 0, // v7-v4-v3-v2 bottom * 0, 0, -1, 0, 0, -1, 0, 0, -1, 0, 0, -1 // v4-v7-v6-v5 back * ], * * // Indices - these organise the positions and and normals * // into geometric primitives in accordance with the "primitive" parameter, * // in this case a set of three indices for each triangle. * // * // Note that each triangle is specified in counter-clockwise winding order. * // * indices: [ * 0, 1, 2, 0, 2, 3, // front * 4, 5, 6, 4, 6, 7, // right * 8, 9, 10, 8, 10, 11, // top * 12, 13, 14, 12, 14, 15, // left * 16, 17, 18, 16, 18, 19, // bottom * 20, 21, 22, 20, 22, 23 * ] * }); * * // Red table leg * * sceneModel.createMesh({ * id: "redLegMesh", * geometryId: "myBoxGeometry", * position: [-4, -6, -4], * scale: [1, 3, 1], * rotation: [0, 0, 0], * color: [1, 0.3, 0.3] * }); * * sceneModel.createEntity({ * id: "redLeg", * meshIds: ["redLegMesh"], * isObject: true // <---- Registers Entity by ID on viewer.scene.objects * }); * * // Green table leg * * sceneModel.createMesh({ * id: "greenLegMesh", * geometryId: "myBoxGeometry", * position: [4, -6, -4], * scale: [1, 3, 1], * rotation: [0, 0, 0], * color: [0.3, 1.0, 0.3] * }); * * sceneModel.createEntity({ * id: "greenLeg", * meshIds: ["greenLegMesh"], * isObject: true // <---- Registers Entity by ID on viewer.scene.objects * }); * * // Blue table leg * * sceneModel.createMesh({ * id: "blueLegMesh", * geometryId: "myBoxGeometry", * position: [4, -6, 4], * scale: [1, 3, 1], * rotation: [0, 0, 0], * color: [0.3, 0.3, 1.0] * }); * * sceneModel.createEntity({ * id: "blueLeg", * meshIds: ["blueLegMesh"], * isObject: true // <---- Registers Entity by ID on viewer.scene.objects * }); * * // Yellow table leg * * sceneModel.createMesh({ * id: "yellowLegMesh", * geometryId: "myBoxGeometry", * position: [-4, -6, 4], * scale: [1, 3, 1], * rotation: [0, 0, 0], * color: [1.0, 1.0, 0.0] * }); * * sceneModel.createEntity({ * id: "yellowLeg", * meshIds: ["yellowLegMesh"], * isObject: true // <---- Registers Entity by ID on viewer.scene.objects * }); * * // Purple table top * * sceneModel.createMesh({ * id: "purpleTableTopMesh", * geometryId: "myBoxGeometry", * position: [0, -3, 0], * scale: [6, 0.5, 6], * rotation: [0, 0, 0], * color: [1.0, 0.3, 1.0] * }); * * sceneModel.createEntity({ * id: "purpleTableTop", * meshIds: ["purpleTableTopMesh"], * isObject: true // <---- Registers Entity by ID on viewer.scene.objects * }); * ```` * * ## Finalizing a SceneModel * * Before we can view and interact with our ````SceneModel````, we need to **finalize** it. Internally, this causes the ````SceneModel```` to build the * vertex buffer objects (VBOs) that support our geometry instances. When using geometry batching (see next example), * this causes ````SceneModel```` to build the VBOs that combine the batched geometries. Note that you can do both instancing and * batching within the same ````SceneModel````. * * Once finalized, we can't add anything more to our ````SceneModel````. * * ```` javascript * SceneModel.finalize(); * ```` * * ## Finding Entities * * As mentioned earlier, {@link Entity} is * the abstract base class for components that represent models, objects, or just * anonymous visible elements. * * Since we created configured our ````SceneModel```` with ````isModel: true````, * we're able to find it as an Entity by ID in ````viewer.scene.models````. Likewise, since * we configured each of its Entities with ````isObject: true````, we're able to * find them in ````viewer.scene.objects````. * * * ````javascript * // Get the whole table model Entity * const table = viewer.scene.models["table"]; * * // Get some leg object Entities * const redLeg = viewer.scene.objects["redLeg"]; * const greenLeg = viewer.scene.objects["greenLeg"]; * const blueLeg = viewer.scene.objects["blueLeg"]; * ```` * * ## Example 2: Geometry Batching * * Let's once more use a ````SceneModel```` * to build the simple table model, this time exploiting geometry batching. * * [![](http://xeokit.io/img/docs/sceneGraph.png)](https://xeokit.github.io/xeokit-sdk/examples/index.html#sceneRepresentation_SceneModel_batching) * * ````javascript * import {Viewer, SceneModel} from "xeokit-sdk.es.js"; * * const viewer = new Viewer({ * canvasId: "myCanvas", * transparent: true * }); * * viewer.scene.camera.eye = [-21.80, 4.01, 6.56]; * viewer.scene.camera.look = [0, -5.75, 0]; * viewer.scene.camera.up = [0.37, 0.91, -0.11]; * * // Create a SceneModel representing a table with four legs, using geometry batching * const sceneModel = new SceneModel(viewer.scene, { * id: "table", * isModel: true, // <--- Registers SceneModel in viewer.scene.models * position: [0, 0, 0], * scale: [1, 1, 1], * rotation: [0, 0, 0] * }); * * // Red table leg * * sceneModel.createMesh({ * id: "redLegMesh", * * // Geometry arrays are same as for the earlier batching example * primitive: "triangles", * positions: [ 1, 1, 1, -1, 1, 1, -1, -1, 1, 1, -1, 1 ... ], * normals: [ 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, ... ], * indices: [ 0, 1, 2, 0, 2, 3, 4, 5, 6, 4, 6, 7, ... ], * position: [-4, -6, -4], * scale: [1, 3, 1], * rotation: [0, 0, 0], * color: [1, 0.3, 0.3] * }); * * sceneModel.createEntity({ * id: "redLeg", * meshIds: ["redLegMesh"], * isObject: true // <---- Registers Entity by ID on viewer.scene.objects * }); * * // Green table leg * * sceneModel.createMesh({ * id: "greenLegMesh", * primitive: "triangles", * primitive: "triangles", * positions: [ 1, 1, 1, -1, 1, 1, -1, -1, 1, 1, -1, 1 ... ], * normals: [ 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, ... ], * indices: [ 0, 1, 2, 0, 2, 3, 4, 5, 6, 4, 6, 7, ... ], * position: [4, -6, -4], * scale: [1, 3, 1], * rotation: [0, 0, 0], * color: [0.3, 1.0, 0.3] * }); * * sceneModel.createEntity({ * id: "greenLeg", * meshIds: ["greenLegMesh"], * isObject: true // <---- Registers Entity by ID on viewer.scene.objects * }); * * // Blue table leg * * sceneModel.createMesh({ * id: "blueLegMesh", * primitive: "triangles", * positions: [ 1, 1, 1, -1, 1, 1, -1, -1, 1, 1, -1, 1 ... ], * normals: [ 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, ... ], * indices: [ 0, 1, 2, 0, 2, 3, 4, 5, 6, 4, 6, 7, ... ], * position: [4, -6, 4], * scale: [1, 3, 1], * rotation: [0, 0, 0], * color: [0.3, 0.3, 1.0] * }); * * sceneModel.createEntity({ * id: "blueLeg", * meshIds: ["blueLegMesh"], * isObject: true // <---- Registers Entity by ID on viewer.scene.objects * }); * * // Yellow table leg object * * sceneModel.createMesh({ * id: "yellowLegMesh", * positions: [ 1, 1, 1, -1, 1, 1, -1, -1, 1, 1, -1, 1 ... ], * normals: [ 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, ... ], * indices: [ 0, 1, 2, 0, 2, 3, 4, 5, 6, 4, 6, 7, ... ], * position: [-4, -6, 4], * scale: [1, 3, 1], * rotation: [0, 0, 0], * color: [1.0, 1.0, 0.0] * }); * * sceneModel.createEntity({ * id: "yellowLeg", * meshIds: ["yellowLegMesh"], * isObject: true // <---- Registers Entity by ID on viewer.scene.objects * }); * * // Purple table top * * sceneModel.createMesh({ * id: "purpleTableTopMesh", * positions: [ 1, 1, 1, -1, 1, 1, -1, -1, 1, 1, -1, 1 ... ], * normals: [ 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, ... ], * indices: [ 0, 1, 2, 0, 2, 3, 4, 5, 6, 4, 6, 7, ... ], * position: [0, -3, 0], * scale: [6, 0.5, 6], * rotation: [0, 0, 0], * color: [1.0, 0.3, 1.0] * }); * * sceneModel.createEntity({ * id: "purpleTableTop", * meshIds: ["purpleTableTopMesh"], * isObject: true // <---- Registers Entity by ID on viewer.scene.objects * }); * * // Finalize the SceneModel. * * SceneModel.finalize(); * * // Find BigModelNodes by their model and object IDs * * // Get the whole table model * const table = viewer.scene.models["table"]; * * // Get some leg objects * const redLeg = viewer.scene.objects["redLeg"]; * const greenLeg = viewer.scene.objects["greenLeg"]; * const blueLeg = viewer.scene.objects["blueLeg"]; * ```` * * ## Classifying with Metadata * * In the previous examples, we used ````SceneModel```` to build * two versions of the same table model, to demonstrate geometry batching and geometry instancing. * * We'll now classify our {@link Entity}s with metadata. This metadata * will work the same for both our examples, since they create the exact same structure of {@link Entity}s * to represent their models and objects. The abstract Entity type is, after all, intended to provide an abstract interface through which differently-implemented scene content can be accessed uniformly. * * To create the metadata, we'll create a {@link MetaModel} for our model, * with a {@link MetaObject} for each of it's objects. The MetaModel and MetaObjects * get the same IDs as the {@link Entity}s that represent their model and objects within our scene. * * ```` javascript * const furnitureMetaModel = viewer.metaScene.createMetaModel("furniture", { // Creates a MetaModel in the MetaScene * * "projectId": "myTableProject", * "revisionId": "V1.0", * * "metaObjects": [ * { // Creates a MetaObject in the MetaModel * "id": "table", * "name": "Table", // Same ID as an object Entity * "type": "furniture", // Arbitrary type, could be IFC type * "properties": { // Arbitrary properties, could be IfcPropertySet * "cost": "200" * } * }, * { * "id": "redLeg", * "name": "Red table Leg", * "type": "leg", * "parent": "table", // References first MetaObject as parent * "properties": { * "material": "wood" * } * }, * { * "id": "greenLeg", // Node with corresponding id does not need to exist * "name": "Green table leg", // and MetaObject does not need to exist for Node with an id * "type": "leg", * "parent": "table", * "properties": { * "material": "wood" * } * }, * { * "id": "blueLeg", * "name": "Blue table leg", * "type": "leg", * "parent": "table", * "properties": { * "material": "wood" * } * }, * { * "id": "yellowLeg", * "name": "Yellow table leg", * "type": "leg", * "parent": "table", * "properties": { * "material": "wood" * } * }, * { * "id": "tableTop", * "name": "Purple table top", * "type": "surface", * "parent": "table", * "properties": { * "material": "formica", * "width": "60", * "depth": "60", * "thickness": "5" * } * } * ] * }); * ```` * * ## Querying Metadata * * Having created and classified our model (either the instancing or batching example), we can now find the {@link MetaModel} * and {@link MetaObject}s using the IDs of their * corresponding {@link Entity}s. * * ````JavaScript * const furnitureMetaModel = scene.metaScene.metaModels["furniture"]; * * const redLegMetaObject = scene.metaScene.metaObjects["redLeg"]; * ```` * * In the snippet below, we'll log metadata on each {@link Entity} we click on: * * ````JavaScript * viewer.scene.input.on("mouseclicked", function (coords) { * * const hit = viewer.scene.pick({ * canvasPos: coords * }); * * if (hit) { * const entity = hit.entity; * const metaObject = viewer.metaScene.metaObjects[entity.id]; * if (metaObject) { * console.log(JSON.stringify(metaObject.getJSON(), null, "\t")); * } * } * }); * ```` * * ## Metadata Structure * * The {@link MetaModel} * organizes its {@link MetaObject}s in * a tree that describes their structural composition: * * ````JavaScript * // Get metadata on the root object * const tableMetaObject = furnitureMetaModel.rootMetaObject; * * // Get metadata on the leg objects * const redLegMetaObject = tableMetaObject.children[0]; * const greenLegMetaObject = tableMetaObject.children[1]; * const blueLegMetaObject = tableMetaObject.children[2]; * const yellowLegMetaObject = tableMetaObject.children[3]; * ```` * * Given an {@link Entity}, we can find the object or model of which it is a part, or the objects that comprise it. We can also generate UI * components from the metadata, such as the tree view demonstrated in [this demo](https://xeokit.github.io/xeokit-sdk/examples/index.html#BIMOffline_glTF_OTCConferenceCenter). * * This hierarchy allows us to express the hierarchical structure of a model while representing it in * various ways in the 3D scene (such as with ````SceneModel````, which * has a non-hierarchical scene representation). * * Note also that a {@link MetaObject} does not need to have a corresponding * {@link Entity} and vice-versa. * * # RTC Coordinates for Double Precision * * ````SceneModel```` can emulate 64-bit precision on GPUs using relative-to-center (RTC) coordinates. * * Consider a model that contains many small objects, but with such large spatial extents that 32 bits of GPU precision (accurate to ~7 digits) will not be sufficient to render all of the the objects without jittering. * * To prevent jittering, we could spatially subdivide the objects into "tiles". Each tile would have a center position, and the positions of the objects within the tile would be relative to that center ("RTC coordinates"). * * While the center positions of the tiles would be 64-bit values, the object positions only need to be 32-bit. * * Internally, when rendering an object with RTC coordinates, xeokit first temporarily translates the camera viewing matrix by the object's tile's RTC center, on the CPU, using 64-bit math. * * Then xeokit loads the viewing matrix into its WebGL shaders, where math happens at 32-bit precision. Within the shaders, the matrix is effectively down-cast to 32-bit precision, and the object's 32-bit vertex positions are transformed by the matrix. * * We see no jittering, because with RTC a detectable loss of GPU accuracy only starts happening to objects as they become very distant from the camera viewpoint, at which point they are too small to be discernible anyway. * * ## RTC Coordinates with Geometry Instancing * * To use RTC with ````SceneModel```` geometry instancing, we specify an RTC center for the geometry via its ````origin```` parameter. Then ````SceneModel```` assumes that all meshes that instance that geometry are within the same RTC coordinate system, ie. the meshes ````position```` and ````rotation```` properties are assumed to be relative to the geometry's ````origin````. * * For simplicity, our example's meshes all instance the same geometry. Therefore, our example model has only one RTC center. * * Note that the axis-aligned World-space boundary (AABB) of our model is ````[ -6, -9, -6, 1000000006, -2.5, 1000000006]````. * * [![](http://xeokit.io/img/docs/sceneGraph.png)](https://xeokit.github.io/xeokit-sdk/examples/index.html#sceneRepresentation_SceneModel_batching) * * ````javascript * const origin = [100000000, 0, 100000000]; * * sceneModel.createGeometry({ * id: "box", * primitive: "triangles", * positions: [ 1, 1, 1, -1, 1, 1, -1, -1, 1, 1, -1, 1 ... ], * normals: [ 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, ... ], * indices: [ 0, 1, 2, 0, 2, 3, 4, 5, 6, 4, 6, 7, ... ], * }); * * sceneModel.createMesh({ * id: "leg1", * geometryId: "box", * position: [-4, -6, -4], * scale: [1, 3, 1], * rotation: [0, 0, 0], * color: [1, 0.3, 0.3], * origin: origin * }); * * sceneModel.createEntity({ * meshIds: ["leg1"], * isObject: true * }); * * sceneModel.createMesh({ * id: "leg2", * geometryId: "box", * position: [4, -6, -4], * scale: [1, 3, 1], * rotation: [0, 0, 0], * color: [0.3, 1.0, 0.3], * origin: origin * }); * * sceneModel.createEntity({ * meshIds: ["leg2"], * isObject: true * }); * * sceneModel.createMesh({ * id: "leg3", * geometryId: "box", * position: [4, -6, 4], * scale: [1, 3, 1], * rotation: [0, 0, 0], * color: [0.3, 0.3, 1.0], * origin: origin * }); * * sceneModel.createEntity({ * meshIds: ["leg3"], * isObject: true * }); * * sceneModel.createMesh({ * id: "leg4", * geometryId: "box", * position: [-4, -6, 4], * scale: [1, 3, 1], * rotation: [0, 0, 0], * color: [1.0, 1.0, 0.0], * origin: origin * }); * * sceneModel.createEntity({ * meshIds: ["leg4"], * isObject: true * }); * * sceneModel.createMesh({ * id: "top", * geometryId: "box", * position: [0, -3, 0], * scale: [6, 0.5, 6], * rotation: [0, 0, 0], * color: [1.0, 0.3, 1.0], * origin: origin * }); * * sceneModel.createEntity({ * meshIds: ["top"], * isObject: true * }); * ```` * * ## RTC Coordinates with Geometry Batching * * To use RTC with ````SceneModel```` geometry batching, we specify an RTC center (````origin````) for each mesh. For performance, we try to have as many meshes share the same value for ````origin```` as possible. Each mesh's ````positions````, ````position```` and ````rotation```` properties are assumed to be relative to ````origin````. * * For simplicity, the meshes in our example all share the same RTC center. * * The axis-aligned World-space boundary (AABB) of our model is ````[ -6, -9, -6, 1000000006, -2.5, 1000000006]````. * * [![](http://xeokit.io/img/docs/sceneGraph.png)](https://xeokit.github.io/xeokit-sdk/examples/index.html#sceneRepresentation_SceneModel_batching) * * ````javascript * const origin = [100000000, 0, 100000000]; * * sceneModel.createMesh({ * id: "leg1", * origin: origin, // This mesh's positions and transforms are relative to the RTC center * primitive: "triangles", * positions: [ 1, 1, 1, -1, 1, 1, -1, -1, 1, 1, -1, 1 ... ], * normals: [ 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, ... ], * indices: [ 0, 1, 2, 0, 2, 3, 4, 5, 6, 4, 6, 7, ... ], * position: [-4, -6, -4], * scale: [1, 3, 1], * rotation: [0, 0, 0], * color: [1, 0.3, 0.3] * }); * * sceneModel.createEntity({ * meshIds: ["leg1"], * isObject: true * }); * * sceneModel.createMesh({ * id: "leg2", * origin: origin, // This mesh's positions and transforms are relative to the RTC center * primitive: "triangles", * positions: [ 1, 1, 1, -1, 1, 1, -1, -1, 1, 1, -1, 1 ... ], * normals: [ 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, ... ], * indices: [ 0, 1, 2, 0, 2, 3, 4, 5, 6, 4, 6, 7, ... ], * position: [4, -6, -4], * scale: [1, 3, 1], * rotation: [0, 0, 0], * color: [0.3, 1.0, 0.3] * }); * * sceneModel.createEntity({ * meshIds: ["leg2"], * isObject: true * }); * * sceneModel.createMesh({ * id: "leg3", * origin: origin, // This mesh's positions and transforms are relative to the RTC center * primitive: "triangles", * positions: [ 1, 1, 1, -1, 1, 1, -1, -1, 1, 1, -1, 1 ... ], * normals: [ 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, ... ], * indices: [ 0, 1, 2, 0, 2, 3, 4, 5, 6, 4, 6, 7, ... ], * position: [4, -6, 4], * scale: [1, 3, 1], * rotation: [0, 0, 0], * color: [0.3, 0.3, 1.0] * }); * * sceneModel.createEntity({ * meshIds: ["leg3"], * isObject: true * }); * * sceneModel.createMesh({ * id: "leg4", * origin: origin, // This mesh's positions and transforms are relative to the RTC center * primitive: "triangles", * positions: [ 1, 1, 1, -1, 1, 1, -1, -1, 1, 1, -1, 1 ... ], * normals: [ 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, ... ], * indices: [ 0, 1, 2, 0, 2, 3, 4, 5, 6, 4, 6, 7, ... ], * position: [-4, -6, 4], * scale: [1, 3, 1], * rotation: [0, 0, 0], * color: [1.0, 1.0, 0.0] * }); * * sceneModel.createEntity({ * meshIds: ["leg4"], * isObject: true * }); * * sceneModel.createMesh({ * id: "top", * origin: origin, // This mesh's positions and transforms are relative to the RTC center * primitive: "triangles", * positions: [ 1, 1, 1, -1, 1, 1, -1, -1, 1, 1, -1, 1 ... ], * normals: [ 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, ... ], * indices: [ 0, 1, 2, 0, 2, 3, 4, 5, 6, 4, 6, 7, ... ], * position: [0, -3, 0], * scale: [6, 0.5, 6], * rotation: [0, 0, 0], * color: [1.0, 0.3, 1.0] * }); * * sceneModel.createEntity({ * meshIds: ["top"], * isObject: true * }); * ```` * * ## Positioning at World-space coordinates * * To position a SceneModel at given double-precision World coordinates, we can * configure the ````origin```` of the SceneModel itself. The ````origin```` is a double-precision * 3D World-space position at which the SceneModel will be located. * * Note that ````position```` is a single-precision offset relative to ````origin````. * * ````javascript * const origin = [100000000, 0, 100000000]; * * const sceneModel = new SceneModel(viewer.scene, { * id: "table", * isModel: true, * origin: origin, // Everything in this SceneModel is relative to this RTC center * position: [0, 0, 0], * scale: [1, 1, 1], * rotation: [0, 0, 0] * }); * * sceneModel.createGeometry({ * id: "box", * primitive: "triangles", * positions: [ 1, 1, 1, -1, 1, 1, -1, -1, 1, 1, -1, 1 ... ], * normals: [ 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, ... ], * indices: [ 0, 1, 2, 0, 2, 3, 4, 5, 6, 4, 6, 7, ... ], * }); * * sceneModel.createMesh({ * id: "leg1", * geometryId: "box", * position: [-4, -6, -4], * scale: [1, 3, 1], * rotation: [0, 0, 0], * color: [1, 0.3, 0.3] * }); * * sceneModel.createEntity({ * meshIds: ["leg1"], * isObject: true * }); * * sceneModel.createMesh({ * id: "leg2", * geometryId: "box", * position: [4, -6, -4], * scale: [1, 3, 1], * rotation: [0, 0, 0], * color: [0.3, 1.0, 0.3] * }); * * sceneModel.createEntity({ * meshIds: ["leg2"], * isObject: true * }); * * sceneModel.createMesh({ * id: "leg3", * geometryId: "box", * position: [4, -6, 4], * scale: [1, 3, 1], * rotation: [0, 0, 0], * color: [0.3, 0.3, 1.0] * }); * * sceneModel.createEntity({ * meshIds: ["leg3"], * isObject: true * }); * * sceneModel.createMesh({ * id: "leg4", * geometryId: "box", * position: [-4, -6, 4], * scale: [1, 3, 1], * rotation: [0, 0, 0], * color: [1.0, 1.0, 0.0] * }); * * sceneModel.createEntity({ * meshIds: ["leg4"], * isObject: true * }); * * sceneModel.createMesh({ * id: "top", * geometryId: "box", * position: [0, -3, 0], * scale: [6, 0.5, 6], * rotation: [0, 0, 0], * color: [1.0, 0.3, 1.0] * }); * * sceneModel.createEntity({ * meshIds: ["top"], * isObject: true * }); * ```` * * # Textures * * ## Loading KTX2 Texture Files into a SceneModel * * A {@link SceneModel} that is configured with a {@link KTX2TextureTranscoder} will * allow us to load textures into it from KTX2 buffers or files. * * In the example below, we'll create a {@link Viewer}, containing a {@link SceneModel} configured with a * {@link KTX2TextureTranscoder}. We'll then programmatically create a simple object within the SceneModel, consisting of * a single mesh with a texture loaded from a KTX2 file, which our SceneModel internally transcodes, using * its {@link KTX2TextureTranscoder}. Note how we configure our {@link KTX2TextureTranscoder} with a path to the Basis Universal * transcoder WASM module. * * ````javascript * const viewer = new Viewer({ * canvasId: "myCanvas", * transparent: true * }); * * viewer.scene.camera.eye = [-21.80, 4.01, 6.56]; * viewer.scene.camera.look = [0, -5.75, 0]; * viewer.scene.camera.up = [0.37, 0.91, -0.11]; * * const textureTranscoder = new KTX2TextureTranscoder({ * viewer, * transcoderPath: "https://cdn.jsdelivr.net/npm/@xeokit/xeokit-sdk/dist/basis/" // <------ Path to BasisU transcoder module * }); * * const sceneModel = new SceneModel(viewer.scene, { * id: "myModel", * textureTranscoder // <<-------------------- Configure model with our transcoder * }); * * sceneModel.createTexture({ * id: "myColorTexture", * src: "../assets/textures/compressed/sample_uastc_zstd.ktx2" // <<----- KTX2 texture asset * }); * * sceneModel.createTexture({ * id: "myMetallicRoughnessTexture", * src: "../assets/textures/alpha/crosshatchAlphaMap.jpg" // <<----- JPEG texture asset * }); * * sceneModel.createTextureSet({ * id: "myTextureSet", * colorTextureId: "myColorTexture", * metallicRoughnessTextureId: "myMetallicRoughnessTexture" * }); * * sceneModel.createMesh({ * id: "myMesh", * textureSetId: "myTextureSet", * primitive: "triangles", * positions: [1, 1, 1, ...], * normals: [0, 0, 1, 0, ...], * uv: [1, 0, 0, ...], * indices: [0, 1, 2, ...], * }); * * sceneModel.createEntity({ * id: "myEntity", * meshIds: ["myMesh"] * }); * * sceneModel.finalize(); * ```` * * ## Loading KTX2 Textures from ArrayBuffers into a SceneModel * * A SceneModel that is configured with a {@link KTX2TextureTranscoder} will allow us to load textures into * it from KTX2 ArrayBuffers. * * In the example below, we'll create a {@link Viewer}, containing a {@link SceneModel} configured with a * {@link KTX2TextureTranscoder}. We'll then programmatically create a simple object within the SceneModel, consisting of * a single mesh with a texture loaded from a KTX2 ArrayBuffer, which our SceneModel internally transcodes, using * its {@link KTX2TextureTranscoder}. * * ````javascript * const viewer = new Viewer({ * canvasId: "myCanvas", * transparent: true * }); * * viewer.scene.camera.eye = [-21.80, 4.01, 6.56]; * viewer.scene.camera.look = [0, -5.75, 0]; * viewer.scene.camera.up = [0.37, 0.91, -0.11]; * * const textureTranscoder = new KTX2TextureTranscoder({ * viewer, * transcoderPath: "https://cdn.jsdelivr.net/npm/@xeokit/xeokit-sdk/dist/basis/" // <------ Path to BasisU transcoder module * }); * * const sceneModel = new SceneModel(viewer.scene, { * id: "myModel", * textureTranscoder // <<-------------------- Configure model with our transcoder * }); * * utils.loadArraybuffer("../assets/textures/compressed/sample_uastc_zstd.ktx2",(arrayBuffer) => { * * sceneModel.createTexture({ * id: "myColorTexture", * buffers: [arrayBuffer] // <<----- KTX2 texture asset * }); * * sceneModel.createTexture({ * id: "myMetallicRoughnessTexture", * src: "../assets/textures/alpha/crosshatchAlphaMap.jpg" // <<----- JPEG texture asset * }); * * sceneModel.createTextureSet({ * id: "myTextureSet", * colorTextureId: "myColorTexture", * metallicRoughnessTextureId: "myMetallicRoughnessTexture" * }); * * sceneModel.createMesh({ * id: "myMesh", * textureSetId: "myTextureSet", * primitive: "triangles", * positions: [1, 1, 1, ...], * normals: [0, 0, 1, 0, ...], * uv: [1, 0, 0, ...], * indices: [0, 1, 2, ...], * }); * * sceneModel.createEntity({ * id: "myEntity", * meshIds: ["myMesh"] * }); * * sceneModel.finalize(); * }); * ```` * * @implements {Entity} */ class SceneModel extends Component { /** * @constructor * @param {Component} owner Owner component. When destroyed, the owner will destroy this component as well. * @param {*} [cfg] Configs * @param {String} [cfg.id] Optional ID, unique among all components in the parent scene, generated automatically when omitted. * @param {Boolean} [cfg.isModel] Specify ````true```` if this SceneModel represents a model, in which case the SceneModel will be registered by {@link SceneModel#id} in {@link Scene#models} and may also have a corresponding {@link MetaModel} with matching {@link MetaModel#id}, registered by that ID in {@link MetaScene#metaModels}. * @param {Number[]} [cfg.origin=[0,0,0]] World-space double-precision 3D origin. * @param {Number[]} [cfg.position=[0,0,0]] Local, single-precision 3D position, relative to the origin parameter. * @param {Number[]} [cfg.scale=[1,1,1]] Local scale. * @param {Number[]} [cfg.rotation=[0,0,0]] Local rotation, as Euler angles given in degrees, for each of the X, Y and Z axis. * @param {Number[]} [cfg.matrix=[1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1] Local modelling transform matrix. Overrides the position, scale and rotation parameters. * @param {Boolean} [cfg.visible=true] Indicates if the SceneModel is initially visible. * @param {Boolean} [cfg.culled=false] Indicates if the SceneModel is initially culled from view. * @param {Boolean} [cfg.pickable=true] Indicates if the SceneModel is initially pickable. * @param {Boolean} [cfg.clippable=true] Indicates if the SceneModel is initially clippable. * @param {Boolean} [cfg.collidable=true] Indicates if the SceneModel is initially included in boundary calculations. * @param {Boolean} [cfg.xrayed=false] Indicates if the SceneModel is initially xrayed. * @param {Boolean} [cfg.highlighted=false] Indicates if the SceneModel is initially highlighted. * @param {Boolean} [cfg.selected=false] Indicates if the SceneModel is initially selected. * @param {Boolean} [cfg.edges=false] Indicates if the SceneModel's edges are initially emphasized. * @param {Number[]} [cfg.colorize=[1.0,1.0,1.0]] SceneModel's initial RGB colorize color, multiplies by the rendered fragment colors. * @param {Number} [cfg.opacity=1.0] SceneModel's initial opacity factor, multiplies by the rendered fragment alpha. * @param {Number} [cfg.backfaces=false] When we set this ````true````, then we force rendering of backfaces for this SceneModel. When * we leave this ````false````, then we allow the Viewer to decide when to render backfaces. In that case, the * Viewer will hide backfaces on watertight meshes, show backfaces on open meshes, and always show backfaces on meshes when we slice them open with {@link SectionPlane}s. * @param {Boolean} [cfg.saoEnabled=true] Indicates if Scalable Ambient Obscurance (SAO) will apply to this SceneModel. SAO is configured by the Scene's {@link SAO} component. * @param {Boolean} [cfg.pbrEnabled=true] Indicates if physically-based rendering (PBR) will apply to the SceneModel when {@link Scene#pbrEnabled} is ````true````. * @param {Boolean} [cfg.colorTextureEnabled=true] Indicates if base color textures will be rendered for the SceneModel when {@link Scene#colorTextureEnabled} is ````true````. * @param {Number} [cfg.edgeThreshold=10] When xraying, highlighting, selecting or edging, this is the threshold angle between normals of adjacent triangles, below which their shared wireframe edge is not drawn. * @param {Number} [cfg.maxGeometryBatchSize=50000000] Maximum geometry batch size, as number of vertices. This is optionally supplied * to limit the size of the batched geometry arrays that SceneModel internally creates for batched geometries. * A lower value means less heap allocation/de-allocation while creating/loading batched geometries, but more draw calls and * slower rendering speed. A high value means larger heap allocation/de-allocation while creating/loading, but less draw calls * and faster rendering speed. It's recommended to keep this somewhere roughly between ````50000```` and ````50000000```. * @param {TextureTranscoder} [cfg.textureTranscoder] Transcoder that will be used internally by {@link SceneModel#createTexture} * to convert transcoded texture data. Only required when we'll be providing transcoded data * to {@link SceneModel#createTexture}. We assume that all transcoded texture data added to a ````SceneModel```` * will then in a format supported by this transcoder. * @param {Boolean} [cfg.dtxEnabled=true] When ````true```` (default) use data textures (DTX), where appropriate, to * represent the returned model. Set false to always use vertex buffer objects (VBOs). Note that DTX is only applicable * to non-textured triangle meshes, and that VBOs are always used for meshes that have textures, line segments, or point * primitives. Only works while {@link DTX#enabled} is also ````true````. * @param {Number} [cfg.renderOrder=0] Specifies the rendering order for this SceneModel. This is used to control the order in which * SceneModels are drawn when they have transparent objects, to give control over the order in which those objects are blended within the transparent * render pass. */ constructor(owner, cfg = {}) { super(owner, cfg); this.renderOrder = cfg.renderOrder || 0; this._dtxEnabled = this.scene.dtxEnabled && (cfg.dtxEnabled !== false); this._enableVertexWelding = false; // Not needed for most objects, and very expensive, so disabled this._enableIndexBucketing = false; // Until fixed: https://github.com/xeokit/xeokit-sdk/issues/1204 this._vboBatchingLayerScratchMemory = getScratchMemory(); this._textureTranscoder = cfg.textureTranscoder || getKTX2TextureTranscoder(this.scene.viewer); this._maxGeometryBatchSize = cfg.maxGeometryBatchSize; this._aabb = math.collapseAABB3(); this._aabbDirty = true; this._quantizationRanges = {}; this._vboInstancingLayers = {}; this._vboBatchingLayers = {}; this._dtxLayers = {}; this._meshList = []; this.layerList = []; // For GL state efficiency when drawing, InstancingLayers are in first part, BatchingLayers are in second this._layersToFinalize = []; this._entityList = []; this._entitiesToFinalize = []; this._geometries = {}; this._dtxBuckets = {}; // Geometries with optimizations used for data texture representation this._textures = {}; this._textureSets = {}; this._transforms = {}; this._meshes = {}; this._unusedMeshes = {}; this._entities = {}; /** @private **/ this.renderFlags = new RenderFlags(); /** * @private */ this.numGeometries = 0; // Number of geometries created with createGeometry() // These counts are used to avoid unnecessary render passes // They are incremented or decremented exclusively by BatchingLayer and InstancingLayer /** * @private */ this.numPortions = 0; /** * @private */ this.numVisibleLayerPortions = 0; /** * @private */ this.numTransparentLayerPortions = 0; /** * @private */ this.numXRayedLayerPortions = 0; /** * @private */ this.numHighlightedLayerPortions = 0; /** * @private */ this.numSelectedLayerPortions = 0; /** * @private */ this.numEdgesLayerPortions = 0; /** * @private */ this.numPickableLayerPortions = 0; /** * @private */ this.numClippableLayerPortions = 0; /** * @private */ this.numCulledLayerPortions = 0; this.numEntities = 0; this._numTriangles = 0; this._numLines = 0; this._numPoints = 0; this._layersFinalized = false; this._edgeThreshold = cfg.edgeThreshold || 10; // Build static matrix this._origin = math.vec3(cfg.origin || [0, 0, 0]); this._position = math.vec3(cfg.position || [0, 0, 0]); this._rotation = math.vec3(cfg.rotation || [0, 0, 0]); this._quaternion = math.vec4(cfg.quaternion || [0, 0, 0, 1]); this._conjugateQuaternion = math.vec4(cfg.quaternion || [0, 0, 0, 1]); if (cfg.rotation) { math.eulerToQuaternion(this._rotation, "XYZ", this._quaternion); } this._scale = math.vec3(cfg.scale || [1, 1, 1]); this._worldRotationMatrix = math.mat4(); this._worldRotationMatrixConjugate = math.mat4(); this._matrix = math.mat4(); this._matrixDirty = true; this._rebuildMatrices(); this._worldNormalMatrix = math.mat4(); math.inverseMat4(this._matrix, this._worldNormalMatrix); math.transposeMat4(this._worldNormalMatrix); if (cfg.matrix || cfg.position || cfg.rotation || cfg.scale || cfg.quaternion) { this._viewMatrix = math.mat4(); this._viewNormalMatrix = math.mat4(); this._viewMatrixDirty = true; this._matrixNonIdentity = true; } this._opacity = 1.0; this._colorize = [1, 1, 1]; this._saoEnabled = (cfg.saoEnabled !== false); this._pbrEnabled = (cfg.pbrEnabled !== false); this._colorTextureEnabled = (cfg.colorTextureEnabled !== false); this._isModel = cfg.isModel; if (this._isModel) { this.scene._registerModel(this); } this._onCameraViewMatrix = this.scene.camera.on("matrix", () => { this._viewMatrixDirty = true; }); this._meshesWithDirtyMatrices = []; this._numMeshesWithDirtyMatrices = 0; this._onTick = this.scene.on("tick", () => { while (this._numMeshesWithDirtyMatrices > 0) { this._meshesWithDirtyMatrices[--this._numMeshesWithDirtyMatrices]._updateMatrix(); } }); this._createDefaultTextureSet(); this.visible = cfg.visible; this.culled = cfg.culled; this.pickable = cfg.pickable; this.clippable = cfg.clippable; this.collidable = cfg.collidable; this.castsShadow = cfg.castsShadow; this.receivesShadow = cfg.receivesShadow; this.xrayed = cfg.xrayed; this.highlighted = cfg.highlighted; this.selected = cfg.selected; this.edges = cfg.edges; this.colorize = cfg.colorize; this.opacity = cfg.opacity; this.backfaces = cfg.backfaces; } _meshMatrixDirty(mesh) { this._meshesWithDirtyMatrices[this._numMeshesWithDirtyMatrices++] = mesh; } _createDefaultTextureSet() { // Every SceneModelMesh gets at least the default TextureSet, // which contains empty default textures filled with color const defaultColorTexture = new SceneModelTexture({ id: DEFAULT_COLOR_TEXTURE_ID, texture: new Texture2D({ gl: this.scene.canvas.gl, preloadColor: [1, 1, 1, 1] // [r, g, b, a]}) }) }); const defaultMetalRoughTexture = new SceneModelTexture({ id: DEFAULT_METAL_ROUGH_TEXTURE_ID, texture: new Texture2D({ gl: this.scene.canvas.gl, preloadColor: [0, 1, 1, 1] // [unused, roughness, metalness, unused] }) }); const defaultNormalsTexture = new SceneModelTexture({ id: DEFAULT_NORMALS_TEXTURE_ID, texture: new Texture2D({ gl: this.scene.canvas.gl, preloadColor: [0, 0, 0, 0] // [x, y, z, unused] - these must be zeros }) }); const defaultEmissiveTexture = new SceneModelTexture({ id: DEFAULT_EMISSIVE_TEXTURE_ID, texture: new Texture2D({ gl: this.scene.canvas.gl, preloadColor: [0, 0, 0, 1] // [x, y, z, unused] }) }); const defaultOcclusionTexture = new SceneModelTexture({ id: DEFAULT_OCCLUSION_TEXTURE_ID, texture: new Texture2D({ gl: this.scene.canvas.gl, preloadColor: [1, 1, 1, 1] // [x, y, z, unused] }) }); this._textures[DEFAULT_COLOR_TEXTURE_ID] = defaultColorTexture; this._textures[DEFAULT_METAL_ROUGH_TEXTURE_ID] = defaultMetalRoughTexture; this._textures[DEFAULT_NORMALS_TEXTURE_ID] = defaultNormalsTexture; this._textures[DEFAULT_EMISSIVE_TEXTURE_ID] = defaultEmissiveTexture; this._textures[DEFAULT_OCCLUSION_TEXTURE_ID] = defaultOcclusionTexture; this._textureSets[DEFAULT_TEXTURE_SET_ID] = new SceneModelTextureSet({ id: DEFAULT_TEXTURE_SET_ID, model: this, colorTexture: defaultColorTexture, metallicRoughnessTexture: defaultMetalRoughTexture, normalsTexture: defaultNormalsTexture, emissiveTexture: defaultEmissiveTexture, occlusionTexture: defaultOcclusionTexture }); } //------------------------------------------------------------------------------------------------------------------ // SceneModel members //------------------------------------------------------------------------------------------------------------------ /** * Returns true to indicate that this Component is a SceneModel. * @type {Boolean} */ get isPerformanceModel() { return true; } /** * The {@link SceneModelTransform}s in this SceneModel. * * Each {#link SceneModelTransform} is stored here against its {@link SceneModelTransform.id}. * * @returns {*|{}} */ get transforms() { return this._transforms; } /** * The {@link SceneModelTexture}s in this SceneModel. * * * Each {@link SceneModelTexture} is created with {@link SceneModel.createTexture}. * * Each {@link SceneModelTexture} is stored here against its {@link SceneModelTexture.id}. * * @returns {*|{}} */ get textures() { return this._textures; } /** * The {@link SceneModelTextureSet}s in this SceneModel. * * Each {@link SceneModelTextureSet} is stored here against its {@link SceneModelTextureSet.id}. * * @returns {*|{}} */ get textureSets() { return this._textureSets; } /** * The {@link SceneModelMesh}es in this SceneModel. * * Each {@SceneModelMesh} is stored here against its {@link SceneModelMesh.id}. * * @returns {*|{}} */ get meshes() { return this._meshes; } /** * The {@link SceneModelEntity}s in this SceneModel. * * Each {#link SceneModelEntity} in this SceneModel that represents an object is * stored here against its {@link SceneModelTransform.id}. * * @returns {*|{}} */ get objects() { return this._entities; } /** * Gets the 3D World-space origin for this SceneModel. * * Each {@link SceneModelMesh.origin}, if supplied, is relative to this origin. * * Default value is ````[0,0,0]````. * * @type {Float64Array} */ get origin() { return this._origin; } /** * Sets the SceneModel's local translation. * * Default value is ````[0,0,0]````. * * @type {Number[]} */ set position(value) { this._position.set(value || [0, 0, 0]); this._setWorldMatrixDirty(); this._sceneModelDirty(); this.glRedraw(); } /** * Gets the SceneModel's local translation. * * Default value is ````[0,0,0]````. * * @type {Number[]} */ get position() { return this._position; } /** * Sets the SceneModel's local rotation, as Euler angles given in degrees, for each of the X, Y and Z axis. * * Default value is ````[0,0,0]````. * * @type {Number[]} */ set rotation(value) { this._rotation.set(value || [0, 0, 0]); math.eulerToQuaternion(this._rotation, "XYZ", this._quaternion); this._setWorldMatrixDirty(); this._sceneModelDirty(); this.glRedraw(); } /** * Gets the SceneModel's local rotation, as Euler angles given in degrees, for each of the X, Y and Z axis. * * Default value is ````[0,0,0]````. * * @type {Number[]} */ get rotation() { return this._rotation; } /** * Sets the SceneModel's local rotation quaternion. * * Default value is ````[0,0,0,1]````. * * @type {Number[]} */ set quaternion(value) { this._quaternion.set(value || [0, 0, 0, 1]); math.quaternionToEuler(this._quaternion, "XYZ", this._rotation); this._setWorldMatrixDirty(); this._sceneModelDirty(); this.glRedraw(); } /** * Gets the SceneModel's local rotation quaternion. * * Default value is ````[0,0,0,1]````. * * @type {Number[]} */ get quaternion() { return this._quaternion; } /** * Sets the SceneModel's local scale. * * Default value is ````[1,1,1]````. * * @type {Number[]} * @deprecated */ set scale(value) { // NOP - deprecated } /** * Gets the SceneModel's local scale. * * Default value is ````[1,1,1]````. * * @type {Number[]} * @deprecated */ get scale() { return this._scale; } /** * Sets the SceneModel's local modeling transform matrix. * * Default value is ````[1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]````. * * @type {Number[]} */ set matrix(value) { this._matrix.set(value || DEFAULT_MATRIX); math.decomposeMat4(this._matrix, this._position, this._quaternion, this._scale); math.quaternionToEuler(this._quaternion, "XYZ", this._rotation); this._matrixDirty = false; this._setWorldMatrixDirty(); this._sceneModelDirty(); this.glRedraw(); } /** * Gets the SceneModel's local modeling transform matrix. * * Default value is ````[1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]````. * * @type {Number[]} */ get matrix() { this._rebuildMatrices(); return this._matrix; } /** * Gets the SceneModel's local modeling rotation transform matrix. * * @type {Number[]} */ get rotationMatrix() { this._rebuildMatrices(); return this._worldRotationMatrix; } _rebuildMatrices() { if (this._matrixDirty) { math.quaternionToRotationMat4(this._quaternion, this._worldRotationMatrix); math.conjugateQuaternion(this._quaternion, this._conjugateQuaternion); math.quaternionToRotationMat4(this._conjugateQuaternion, this._worldRotationMatrixConjugate); math.scaleMat4v(this._scale, this._worldRotationMatrix); math.scaleMat4v(this._scale, this._worldRotationMatrixConjugate); this._matrix.set(this._worldRotationMatrix); math.translateMat4v(this._position, this._matrix); this._matrixDirty = false; this._viewMatrixDirty = true; } } /** * Gets the conjugate of the SceneModel's local modeling rotation transform matrix. * * This is used for RTC view matrix management in renderers. * * @type {Number[]} */ get rotationMatrixConjugate() { this._rebuildMatrices(); return this._worldRotationMatrixConjugate; } _setWorldMatrixDirty() { this._matrixDirty = true; this._aabbDirty = true; } _transformDirty() { this._matrixDirty = true; this._aabbDirty = true; this.scene._aabbDirty = true; } _sceneModelDirty() { this.scene._aabbDirty = true; this._aabbDirty = true; this.scene._aabbDirty = true; this._matrixDirty = true; for (let i = 0, len = this._entityList.length; i < len; i++) { this._entityList[i]._sceneModelDirty(); // Entities need to retransform their World AABBs by SceneModel's worldMatrix } } /** * Gets the SceneModel's World matrix. * * @property worldMatrix * @type {Number[]} */ get worldMatrix() { return this.matrix; } /** * Gets the SceneModel's World normal matrix. * * @type {Number[]} */ get worldNormalMatrix() { return this._worldNormalMatrix; } _rebuildViewMatrices() { this._rebuildMatrices(); if (this._viewMatrixDirty) { math.mulMat4(this.scene.camera.viewMatrix, this._matrix, this._viewMatrix); math.inverseMat4(this._viewMatrix, this._viewNormalMatrix); math.transposeMat4(this._viewNormalMatrix); this._viewMatrixDirty = false; } } /** * Called by private renderers in ./lib, returns the view matrix with which to * render this SceneModel. The view matrix is the concatenation of the * Camera view matrix with the Performance model's world (modeling) matrix. * * @private */ get viewMatrix() { if (!this._viewMatrix) { return this.scene.camera.viewMatrix; } this._rebuildViewMatrices(); return this._viewMatrix; } /** * Called by private renderers in ./lib, returns the view normal matrix with which to render this SceneModel. * * @private */ get viewNormalMatrix() { if (!this._viewNormalMatrix) { return this.scene.camera.viewNormalMatrix; } this._rebuildViewMatrices(); return this._viewNormalMatrix; } /** * Sets if backfaces are rendered for this SceneModel. * * Default is ````false````. * * @type {Boolean} */ get backfaces() { return this._backfaces; } /** * Sets if backfaces are rendered for this SceneModel. * * Default is ````false````. * * When we set this ````true````, then backfaces are always rendered for this SceneModel. * * When we set this ````false````, then we allow the Viewer to decide whether to render backfaces. In this case, * the Viewer will: * * * hide backfaces on watertight meshes, * * show backfaces on open meshes, and * * always show backfaces on meshes when we slice them open with {@link SectionPlane}s. * * @type {Boolean} */ set backfaces(backfaces) { backfaces = !!backfaces; this._backfaces = backfaces; this.glRedraw(); } /** * Gets the list of {@link SceneModelEntity}s within this SceneModel. * * @returns {SceneModelEntity[]} */ get entityList() { return this._entityList; } /** * Returns true to indicate that SceneModel is an {@link Entity}. * @type {Boolean} */ get isEntity() { return true; } /** * Returns ````true```` if this SceneModel represents a model. * * When ````true```` the SceneModel will be registered by {@link SceneModel#id} in * {@link Scene#models} and may also have a {@link MetaObject} with matching {@link MetaObject#id}. * * @type {Boolean} */ get isModel() { return this._isModel; } //------------------------------------------------------------------------------------------------------------------ // SceneModel members //------------------------------------------------------------------------------------------------------------------ /** * Returns ````false```` to indicate that SceneModel never represents an object. * * @type {Boolean} */ get isObject() { return false; } /** * Gets the SceneModel's World-space 3D axis-aligned bounding box. * * Represented by a six-element Float64Array containing the min/max extents of the * axis-aligned volume, ie. ````[xmin, ymin,zmin,xmax,ymax, zmax]````. * * @type {Number[]} */ get aabb() { if (this._aabbDirty) { math.collapseAABB3(this._aabb); for (let i = 0, len = this._entityList.length; i < len; i++) { math.expandAABB3(this._aabb, this._entityList[i].aabb); } this._aabbDirty = false; } return this._aabb; } /** * The approximate number of triangle primitives in this SceneModel. * * @type {Number} */ get numTriangles() { return this._numTriangles; } //------------------------------------------------------------------------------------------------------------------ // Entity members //------------------------------------------------------------------------------------------------------------------ /** * The approximate number of line primitives in this SceneModel. * * @type {Number} */ get numLines() { return this._numLines; } /** * The approximate number of point primitives in this SceneModel. * * @type {Number} */ get numPoints() { return this._numPoints; } /** * Gets if any {@link SceneModelEntity}s in this SceneModel are visible. * * The SceneModel is only rendered when {@link SceneModel#visible} is ````true```` and {@link SceneModel#culled} is ````false````. * * @type {Boolean} */ get visible() { return (this.numVisibleLayerPortions > 0); } /** * Sets if this SceneModel is visible. * * The SceneModel is only rendered when {@link SceneModel#visible} is ````true```` and {@link SceneModel#culled} is ````false````. ** * @type {Boolean} */ set visible(visible) { visible = visible !== false; this._visible = visible; for (let i = 0, len = this._entityList.length; i < len; i++) { this._entityList[i].visible = visible; } this.glRedraw(); } /** * Gets if any {@link SceneModelEntity}s in this SceneModel are xrayed. * * @type {Boolean} */ get xrayed() { return (this.numXRayedLayerPortions > 0); } /** * Sets if all {@link SceneModelEntity}s in this SceneModel are xrayed. * * @type {Boolean} */ set xrayed(xrayed) { xrayed = !!xrayed; this._xrayed = xrayed; for (let i = 0, len = this._entityList.length; i < len; i++) { this._entityList[i].xrayed = xrayed; } this.glRedraw(); } /** * Gets if any {@link SceneModelEntity}s in this SceneModel are highlighted. * * @type {Boolean} */ get highlighted() { return (this.numHighlightedLayerPortions > 0); } /** * Sets if all {@link SceneModelEntity}s in this SceneModel are highlighted. * * @type {Boolean} */ set highlighted(highlighted) { highlighted = !!highlighted; this._highlighted = highlighted; for (let i = 0, len = this._entityList.length; i < len; i++) { this._entityList[i].highlighted = highlighted; } this.glRedraw(); } /** * Gets if any {@link SceneModelEntity}s in this SceneModel are selected. * * @type {Boolean} */ get selected() { return (this.numSelectedLayerPortions > 0); } /** * Sets if all {@link SceneModelEntity}s in this SceneModel are selected. * * @type {Boolean} */ set selected(selected) { selected = !!selected; this._selected = selected; for (let i = 0, len = this._entityList.length; i < len; i++) { this._entityList[i].selected = selected; } this.glRedraw(); } /** * Gets if any {@link SceneModelEntity}s in this SceneModel have edges emphasised. * * @type {Boolean} */ get edges() { return (this.numEdgesLayerPortions > 0); } /** * Sets if all {@link SceneModelEntity}s in this SceneModel have edges emphasised. * * @type {Boolean} */ set edges(edges) { edges = !!edges; this._edges = edges; for (let i = 0, len = this._entityList.length; i < len; i++) { this._entityList[i].edges = edges; } this.glRedraw(); } /** * Gets if this SceneModel is culled from view. * * The SceneModel is only rendered when {@link SceneModel#visible} is true and {@link SceneModel#culled} is false. * * @type {Boolean} */ get culled() { return this._culled; } /** * Sets if this SceneModel is culled from view. * * The SceneModel is only rendered when {@link SceneModel#visible} is true and {@link SceneModel#culled} is false. * * @type {Boolean} */ set culled(culled) { culled = !!culled; this._culled = culled; for (let i = 0, len = this._entityList.length; i < len; i++) { this._entityList[i].culled = culled; } this.glRedraw(); } /** * Gets if {@link SceneModelEntity}s in this SceneModel are clippable. * * Clipping is done by the {@link SectionPlane}s in {@link Scene#sectionPlanes}. * * @type {Boolean} */ get clippable() { return this._clippable; } /** * Sets if {@link SceneModelEntity}s in this SceneModel are clippable. * * Clipping is done by the {@link SectionPlane}s in {@link Scene#sectionPlanes}. * * @type {Boolean} */ set clippable(clippable) { clippable = clippable !== false; this._clippable = clippable; for (let i = 0, len = this._entityList.length; i < len; i++) { this._entityList[i].clippable = clippable; } this.glRedraw(); } /** * Gets if this SceneModel is collidable. * * @type {Boolean} */ get collidable() { return this._collidable; } /** * Sets if {@link SceneModelEntity}s in this SceneModel are collidable. * * @type {Boolean} */ set collidable(collidable) { collidable = collidable !== false; this._collidable = collidable; for (let i = 0, len = this._entityList.length; i < len; i++) { this._entityList[i].collidable = collidable; } } /** * Gets if this SceneModel is pickable. * * Picking is done via calls to {@link Scene#pick}. * * @type {Boolean} */ get pickable() { return (this.numPickableLayerPortions > 0); } /** * Sets if {@link SceneModelEntity}s in this SceneModel are pickable. * * Picking is done via calls to {@link Scene#pick}. * * @type {Boolean} */ set pickable(pickable) { pickable = pickable !== false; this._pickable = pickable; for (let i = 0, len = this._entityList.length; i < len; i++) { this._entityList[i].pickable = pickable; } } /** * Gets the RGB colorize color for this SceneModel. * * Each element of the color is in range ````[0..1]````. * * @type {Number[]} */ get colorize() { return this._colorize; } /** * Sets the RGB colorize color for this SceneModel. * * Multiplies by rendered fragment colors. * * Each element of the color is in range ````[0..1]````. * * @type {Number[]} */ set colorize(colorize) { this._colorize = colorize; for (let i = 0, len = this._entityList.length; i < len; i++) { this._entityList[i].colorize = colorize; } } /** * Gets this SceneModel's opacity factor. * * This is a factor in range ````[0..1]```` which multiplies by the rendered fragment alphas. * * @type {Number} */ get opacity() { return this._opacity; } /** * Sets the opacity factor for this SceneModel. * * This is a factor in range ````[0..1]```` which multiplies by the rendered fragment alphas. * * @type {Number} */ set opacity(opacity) { this._opacity = opacity; for (let i = 0, len = this._entityList.length; i < len; i++) { this._entityList[i].opacity = opacity; } } /** * Gets if this SceneModel casts a shadow. * * @type {Boolean} */ get castsShadow() { return this._castsShadow; } /** * Sets if this SceneModel casts a shadow. * * @type {Boolean} */ set castsShadow(castsShadow) { castsShadow = (castsShadow !== false); if (castsShadow !== this._castsShadow) { this._castsShadow = castsShadow; this.glRedraw(); } } /** * Sets if this SceneModel can have shadow cast upon it. * * @type {Boolean} */ get receivesShadow() { return this._receivesShadow; } /** * Sets if this SceneModel can have shadow cast upon it. * * @type {Boolean} */ set receivesShadow(receivesShadow) { receivesShadow = (receivesShadow !== false); if (receivesShadow !== this._receivesShadow) { this._receivesShadow = receivesShadow; this.glRedraw(); } } /** * Gets if Scalable Ambient Obscurance (SAO) will apply to this SceneModel. * * SAO is configured by the Scene's {@link SAO} component. * * Only works when {@link SAO#enabled} is also true. * * @type {Boolean} */ get saoEnabled() { return this._saoEnabled; } /** * Gets if physically-based rendering (PBR) is enabled for this SceneModel. * * Only works when {@link Scene#pbrEnabled} is also true. * * @type {Boolean} */ get pbrEnabled() { return this._pbrEnabled; } /** * Gets if color textures are enabled for this SceneModel. * * Only works when {@link Scene#colorTextureEnabled} is also true. * * @type {Boolean} */ get colorTextureEnabled() { return this._colorTextureEnabled; } /** * Returns true to indicate that SceneModel is implements {@link Drawable}. * * @type {Boolean} */ get isDrawable() { return true; } /** @private */ get isStateSortable() { return false } /** * Configures the appearance of xrayed {@link SceneModelEntity}s within this SceneModel. * * This is the {@link Scene#xrayMaterial}. * * @type {EmphasisMaterial} */ get xrayMaterial() { return this.scene.xrayMaterial; } /** * Configures the appearance of highlighted {@link SceneModelEntity}s within this SceneModel. * * This is the {@link Scene#highlightMaterial}. * * @type {EmphasisMaterial} */ get highlightMaterial() { return this.scene.highlightMaterial; } /** * Configures the appearance of selected {@link SceneModelEntity}s within this SceneModel. * * This is the {@link Scene#selectedMaterial}. * * @type {EmphasisMaterial} */ get selectedMaterial() { return this.scene.selectedMaterial; } /** * Configures the appearance of edges of {@link SceneModelEntity}s within this SceneModel. * * This is the {@link Scene#edgeMaterial}. * * @type {EdgeMaterial} */ get edgeMaterial() { return this.scene.edgeMaterial; } //------------------------------------------------------------------------------------------------------------------ // Drawable members //------------------------------------------------------------------------------------------------------------------ /** * Called by private renderers in ./lib, returns the picking view matrix with which to * ray-pick on this SceneModel. * * @private */ getPickViewMatrix(pickViewMatrix) { if (!this._viewMatrix) { return pickViewMatrix; } return this._viewMatrix; } /** * * @param cfg */ createQuantizationRange(cfg) { if (cfg.id === undefined || cfg.id === null) { this.error("[createQuantizationRange] Config missing: id"); return; } if (cfg.aabb) { this.error("[createQuantizationRange] Config missing: aabb"); return; } if (this._quantizationRanges[cfg.id]) { this.error("[createQuantizationRange] QuantizationRange already created: " + cfg.id); return; } this._quantizationRanges[cfg.id] = { id: cfg.id, aabb: cfg.aabb, matrix: createPositionsDecodeMatrix(cfg.aabb, math.mat4()) }; } /** * Creates a reusable geometry within this SceneModel. * * We can then supply the geometry ID to {@link SceneModel#createMesh} when we want to create meshes that * instance the geometry. * * @param {*} cfg Geometry properties. * @param {String|Number} cfg.id Mandatory ID for the geometry, to refer to with {@link SceneModel#createMesh}. * @param {String} cfg.primitive The primitive type. Accepted values are 'points', 'lines', 'triangles', 'solid' and 'surface'. * @param {Number[]} [cfg.positions] Flat array of uncompressed 3D vertex positions positions. Required for all primitive types. Overridden by ````positionsCompressed````. * @param {Number[]} [cfg.positionsCompressed] Flat array of quantized 3D vertex positions. Overrides ````positions````, and must be accompanied by ````positionsDecodeMatrix````. * @param {Number[]} [cfg.positionsDecodeMatrix] A 4x4 matrix for decompressing ````positionsCompressed````. Must be accompanied by ````positionsCompressed````. * @param {Number[]} [cfg.normals] Flat array of normal vectors. Only used with "triangles", "solid" and "surface" primitives. When no normals are given, the geometry will be flat shaded using auto-generated face-aligned normals. * @param {Number[]} [cfg.normalsCompressed] Flat array of oct-encoded normal vectors. Overrides ````normals````. Only used with "triangles", "solid" and "surface" primitives. When no normals are given, the geometry will be flat shaded using auto-generated face-aligned normals. * @param {Number[]} [cfg.colors] Flat array of uncompressed RGBA vertex colors, as float values in range ````[0..1]````. Ignored when ````geometryId```` is given. Overridden by ````color```` and ````colorsCompressed````. * @param {Number[]} [cfg.colorsCompressed] Flat array of compressed RGBA vertex colors, as unsigned short integers in range ````[0..255]````. Ignored when ````geometryId```` is given. Overrides ````colors```` and is overridden by ````color````. * @param {Number[]} [cfg.uv] Flat array of uncompressed vertex UV coordinates. Only used with "triangles", "solid" and "surface" primitives. Required for textured rendering. * @param {Number[]} [cfg.uvCompressed] Flat array of compressed vertex UV coordinates. Only used with "triangles", "solid" and "surface" primitives. Overrides ````uv````. Must be accompanied by ````uvDecodeMatrix````. Only used with "triangles", "solid" and "surface" primitives. Required for textured rendering. * @param {Number[]} [cfg.uvDecodeMatrix] A 3x3 matrix for decompressing ````uvCompressed````. * @param {Number[]} [cfg.indices] Array of primitive connectivity indices. Not required for `points` primitives. * @param {Number[]} [cfg.edgeIndices] Array of edge line indices. Used only with 'triangles', 'solid' and 'surface' primitives. Automatically generated internally if not supplied, using the optional ````edgeThreshold```` given to the ````SceneModel```` constructor. */ createGeometry(cfg) { if (cfg.id === undefined || cfg.id === null) { this.error("[createGeometry] Config missing: id"); return; } if (this._geometries[cfg.id]) { this.error("[createGeometry] Geometry already created: " + cfg.id); return; } if (cfg.primitive === undefined || cfg.primitive === null) { cfg.primitive = "triangles"; } if (cfg.primitive !== "points" && cfg.primitive !== "lines" && cfg.primitive !== "triangles" && cfg.primitive !== "solid" && cfg.primitive !== "surface") { this.error(`[createGeometry] Unsupported value for 'primitive': '${cfg.primitive}' - supported values are 'points', 'lines', 'triangles', 'solid' and 'surface'. Defaulting to 'triangles'.`); return; } if (!cfg.positions && !cfg.positionsCompressed && !cfg.buckets) { this.error("[createGeometry] Param expected: `positions`, `positionsCompressed' or 'buckets"); return null; } if (cfg.positionsCompressed && !cfg.positionsDecodeMatrix && !cfg.positionsDecodeBoundary) { this.error("[createGeometry] Param expected: `positionsDecodeMatrix` or 'positionsDecodeBoundary' (required for `positionsCompressed')"); return null; } if (cfg.positionsDecodeMatrix && cfg.positionsDecodeBoundary) { this.error("[createGeometry] Only one of these params expected: `positionsDecodeMatrix` or 'positionsDecodeBoundary' (required for `positionsCompressed')"); return null; } if (cfg.uvCompressed && !cfg.uvDecodeMatrix) { this.error("[createGeometry] Param expected: `uvDecodeMatrix` (required for `uvCompressed')"); return null; } if (!cfg.buckets && !cfg.indices && (cfg.primitive === "triangles" || cfg.primitive === "solid" || cfg.primitive === "surface")) { const numPositions = (cfg.positions || cfg.positionsCompressed).length / 3; cfg.indices = this._createDefaultIndices(numPositions); } if (!cfg.buckets && !cfg.indices && cfg.primitive !== "points") { this.error(`[createGeometry] Param expected: indices (required for '${cfg.primitive}' primitive type)`); return null; } if (cfg.positionsDecodeBoundary) { cfg.positionsDecodeMatrix = createPositionsDecodeMatrix(cfg.positionsDecodeBoundary, math.mat4()); } if (cfg.positions) { const aabb = math.collapseAABB3(); cfg.positionsDecodeMatrix = math.mat4(); math.expandAABB3Points3(aabb, cfg.positions); cfg.positionsCompressed = quantizePositions(cfg.positions, aabb, cfg.positionsDecodeMatrix); cfg.aabb = aabb; } else if (cfg.positionsCompressed) { const aabb = math.collapseAABB3(); cfg.positionsDecodeMatrix = new Float64Array(cfg.positionsDecodeMatrix); cfg.positionsCompressed = new Uint16Array(cfg.positionsCompressed); math.expandAABB3Points3(aabb, cfg.positionsCompressed); geometryCompressionUtils.decompressAABB(aabb, cfg.positionsDecodeMatrix); cfg.aabb = aabb; } else if (cfg.buckets) { const aabb = math.collapseAABB3(); this._dtxBuckets[cfg.id] = cfg.buckets; for (let i = 0, len = cfg.buckets.length; i < len; i++) { const bucket = cfg.buckets[i]; if (bucket.positions) { math.expandAABB3Points3(aabb, bucket.positions); } else if (bucket.positionsCompressed) { math.expandAABB3Points3(aabb, bucket.positionsCompressed); } } if (cfg.positionsDecodeMatrix) { geometryCompressionUtils.decompressAABB(aabb, cfg.positionsDecodeMatrix); } cfg.aabb = aabb; } if (cfg.colorsCompressed && cfg.colorsCompressed.length > 0) { cfg.colorsCompressed = new Uint8Array(cfg.colorsCompressed); } else if (cfg.colors && cfg.colors.length > 0) { const colors = cfg.colors; const colorsCompressed = new Uint8Array(colors.length); for (let i = 0, len = colors.length; i < len; i++) { colorsCompressed[i] = colors[i] * 255; } cfg.colorsCompressed = colorsCompressed; } if (!cfg.buckets && !cfg.edgeIndices && (cfg.primitive === "triangles" || cfg.primitive === "solid" || cfg.primitive === "surface")) { if (cfg.positions) { cfg.edgeIndices = buildEdgeIndices(cfg.positions, cfg.indices, null, 5.0); } else { cfg.edgeIndices = buildEdgeIndices(cfg.positionsCompressed, cfg.indices, cfg.positionsDecodeMatrix, 2.0); } } if (cfg.uv) { const bounds = geometryCompressionUtils.getUVBounds(cfg.uv); const result = geometryCompressionUtils.compressUVs(cfg.uv, bounds.min, bounds.max); cfg.uvCompressed = result.quantized; cfg.uvDecodeMatrix = result.decodeMatrix; } else if (cfg.uvCompressed) { cfg.uvCompressed = new Uint16Array(cfg.uvCompressed); cfg.uvDecodeMatrix = new Float64Array(cfg.uvDecodeMatrix); } if (cfg.normals) { // HACK cfg.normals = null; } this._geometries [cfg.id] = cfg; this._numTriangles += (cfg.indices ? Math.round(cfg.indices.length / 3) : 0); this.numGeometries++; } /** * Creates a texture within this SceneModel. * * We can then supply the texture ID to {@link SceneModel#createTextureSet} when we want to create texture sets that use the texture. * * @param {*} cfg Texture properties. * @param {String|Number} cfg.id Mandatory ID for the texture, to refer to with {@link SceneModel#createTextureSet}. * @param {String} [cfg.src] Image file for the texture. Assumed to be transcoded if not having a recognized image file * extension (jpg, jpeg, png etc.). If transcoded, then assumes ````SceneModel```` is configured with a {@link TextureTranscoder}. * @param {ArrayBuffer[]} [cfg.buffers] Transcoded texture data. Assumes ````SceneModel```` is * configured with a {@link TextureTranscoder}. This parameter is given as an array of buffers so we can potentially support multi-image textures, such as cube maps. * @param {HTMLImageElement} [cfg.image] HTML Image object to load into this texture. Overrides ````src```` and ````buffers````. Never transcoded. * @param {Number} [cfg.minFilter=LinearMipmapLinearFilter] How the texture is sampled when a texel covers less than one pixel. * Supported values are {@link LinearMipmapLinearFilter}, {@link LinearMipMapNearestFilter}, {@link NearestMipMapNearestFilter}, {@link NearestMipMapLinearFilter} and {@link LinearMipMapLinearFilter}. * @param {Number} [cfg.magFilter=LinearFilter] How the texture is sampled when a texel covers more than one pixel. Supported values are {@link LinearFilter} and {@link NearestFilter}. * @param {Number} [cfg.wrapS=RepeatWrapping] Wrap parameter for texture coordinate *S*. Supported values are {@link ClampToEdgeWrapping}, {@link MirroredRepeatWrapping} and {@link RepeatWrapping}. * @param {Number} [cfg.wrapT=RepeatWrapping] Wrap parameter for texture coordinate *T*. Supported values are {@link ClampToEdgeWrapping}, {@link MirroredRepeatWrapping} and {@link RepeatWrapping}.. * @param {Number} [cfg.wrapR=RepeatWrapping] Wrap parameter for texture coordinate *R*. Supported values are {@link ClampToEdgeWrapping}, {@link MirroredRepeatWrapping} and {@link RepeatWrapping}. * @param {Boolean} [cfg.flipY=false] Flips this Texture's source data along its vertical axis when ````true````. * @param {Number} [cfg.encoding=LinearEncoding] Encoding format. Supported values are {@link LinearEncoding} and {@link sRGBEncoding}. */ createTexture(cfg) { const textureId = cfg.id; if (textureId === undefined || textureId === null) { this.error("[createTexture] Config missing: id"); return; } if (this._textures[textureId]) { this.error("[createTexture] Texture already created: " + textureId); return; } if (!cfg.src && !cfg.image && !cfg.buffers) { this.error("[createTexture] Param expected: `src`, `image' or 'buffers'"); return null; } let minFilter = cfg.minFilter || LinearMipmapLinearFilter; if (minFilter !== LinearFilter && minFilter !== LinearMipMapNearestFilter && minFilter !== LinearMipmapLinearFilter && minFilter !== NearestMipMapLinearFilter && minFilter !== NearestMipMapNearestFilter) { this.error(`[createTexture] Unsupported value for 'minFilter' - supported values are LinearFilter, LinearMipMapNearestFilter, NearestMipMapNearestFilter, NearestMipMapLinearFilter and LinearMipmapLinearFilter. Defaulting to LinearMipmapLinearFilter.`); minFilter = LinearMipmapLinearFilter; } let magFilter = cfg.magFilter || LinearFilter; if (magFilter !== LinearFilter && magFilter !== NearestFilter) { this.error(`[createTexture] Unsupported value for 'magFilter' - supported values are LinearFilter and NearestFilter. Defaulting to LinearFilter.`); magFilter = LinearFilter; } let wrapS = cfg.wrapS || RepeatWrapping; if (wrapS !== ClampToEdgeWrapping && wrapS !== MirroredRepeatWrapping && wrapS !== RepeatWrapping) { this.error(`[createTexture] Unsupported value for 'wrapS' - supported values are ClampToEdgeWrapping, MirroredRepeatWrapping and RepeatWrapping. Defaulting to RepeatWrapping.`); wrapS = RepeatWrapping; } let wrapT = cfg.wrapT || RepeatWrapping; if (wrapT !== ClampToEdgeWrapping && wrapT !== MirroredRepeatWrapping && wrapT !== RepeatWrapping) { this.error(`[createTexture] Unsupported value for 'wrapT' - supported values are ClampToEdgeWrapping, MirroredRepeatWrapping and RepeatWrapping. Defaulting to RepeatWrapping.`); wrapT = RepeatWrapping; } let wrapR = cfg.wrapR || RepeatWrapping; if (wrapR !== ClampToEdgeWrapping && wrapR !== MirroredRepeatWrapping && wrapR !== RepeatWrapping) { this.error(`[createTexture] Unsupported value for 'wrapR' - supported values are ClampToEdgeWrapping, MirroredRepeatWrapping and RepeatWrapping. Defaulting to RepeatWrapping.`); wrapR = RepeatWrapping; } let encoding = cfg.encoding || LinearEncoding; if (encoding !== LinearEncoding && encoding !== sRGBEncoding) { this.error("[createTexture] Unsupported value for 'encoding' - supported values are LinearEncoding and sRGBEncoding. Defaulting to LinearEncoding."); encoding = LinearEncoding; } const texture = new Texture2D({ gl: this.scene.canvas.gl, minFilter, magFilter, wrapS, wrapT, wrapR, // flipY: cfg.flipY, encoding }); if (cfg.preloadColor) { texture.setPreloadColor(cfg.preloadColor); } if (cfg.image) { // Ignore transcoder for Images const image = cfg.image; image.crossOrigin = "Anonymous"; if (image.compressed) { // see `parsedImage` in @loaders.gl/gltf/src/lib/parsers/parse-gltf.ts // NOTE: @loaders.gl in its current version discards potential mipmaps, leaving only a single one const data = image.data; texture.setCompressedData({ mipmaps: data, props: { format: data[0].format, minFilter: minFilter, magFilter: magFilter } }); } else { texture.setImage(image, {minFilter, magFilter, wrapS, wrapT, wrapR, flipY: cfg.flipY, encoding}); } } else if (cfg.src) { const ext = cfg.src.split('.').pop(); switch (ext) { // Don't transcode recognized image file types case "jpeg": case "jpg": case "png": case "gif": const image = new Image(); image.onload = () => { texture.setImage(image, { minFilter, magFilter, wrapS, wrapT, wrapR, flipY: cfg.flipY, encoding }); this.glRedraw(); }; image.src = cfg.src; // URL or Base64 string break; default: // Assume other file types need transcoding if (!this._textureTranscoder) { this.error(`[createTexture] Can't create texture from 'src' - SceneModel needs to be configured with a TextureTranscoder for this file type ('${ext}')`); } else { utils.loadArraybuffer(cfg.src, (arrayBuffer) => { if (!arrayBuffer.byteLength) { this.error(`[createTexture] Can't create texture from 'src': file data is zero length`); return; } this._textureTranscoder.transcode([arrayBuffer], texture).then(() => { this.glRedraw(); }); }, function (errMsg) { this.error(`[createTexture] Can't create texture from 'src': ${errMsg}`); }); } break; } } else if (cfg.buffers) { // Buffers implicitly require transcoding if (!this._textureTranscoder) { this.error(`[createTexture] Can't create texture from 'buffers' - SceneModel needs to be configured with a TextureTranscoder for this option`); } else { this._textureTranscoder.transcode(cfg.buffers, texture).then(() => { this.glRedraw(); }); } } this._textures[textureId] = new SceneModelTexture({id: textureId, texture}); } /** * Creates a texture set within this SceneModel. * * * Stores the new {@link SceneModelTextureSet} in {@link SceneModel#textureSets}. * * A texture set is a collection of textures that can be shared among meshes. We can then supply the texture set * ID to {@link SceneModel#createMesh} when we want to create meshes that use the texture set. * * The textures can work as a texture atlas, where each mesh can have geometry UVs that index * a different part of the textures. This allows us to minimize the number of textures in our models, which * means faster rendering. * * @param {*} cfg Texture set properties. * @param {String|Number} cfg.id Mandatory ID for the texture set, to refer to with {@link SceneModel#createMesh}. * @param {*} [cfg.colorTextureId] ID of *RGBA* base color texture, with color in *RGB* and alpha in *A*. * @param {*} [cfg.metallicRoughnessTextureId] ID of *RGBA* metal-roughness texture, with the metallic factor in *R*, and roughness factor in *G*. * @param {*} [cfg.normalsTextureId] ID of *RGBA* normal map texture, with normal map vectors in *RGB*. * @param {*} [cfg.emissiveTextureId] ID of *RGBA* emissive map texture, with emissive color in *RGB*. * @param {*} [cfg.occlusionTextureId] ID of *RGBA* occlusion map texture, with occlusion factor in *R*. * @returns {SceneModelTransform} The new texture set. */ createTextureSet(cfg) { const textureSetId = cfg.id; if (textureSetId === undefined || textureSetId === null) { this.error("[createTextureSet] Config missing: id"); return; } if (this._textureSets[textureSetId]) { this.error(`[createTextureSet] Texture set already created: ${textureSetId}`); return; } let colorTexture; if (cfg.colorTextureId !== undefined && cfg.colorTextureId !== null) { colorTexture = this._textures[cfg.colorTextureId]; if (!colorTexture) { this.error(`[createTextureSet] Texture not found: ${cfg.colorTextureId} - ensure that you create it first with createTexture()`); return; } } else { colorTexture = this._textures[DEFAULT_COLOR_TEXTURE_ID]; } let metallicRoughnessTexture; if (cfg.metallicRoughnessTextureId !== undefined && cfg.metallicRoughnessTextureId !== null) { metallicRoughnessTexture = this._textures[cfg.metallicRoughnessTextureId]; if (!metallicRoughnessTexture) { this.error(`[createTextureSet] Texture not found: ${cfg.metallicRoughnessTextureId} - ensure that you create it first with createTexture()`); return; } } else { metallicRoughnessTexture = this._textures[DEFAULT_METAL_ROUGH_TEXTURE_ID]; } let normalsTexture; if (cfg.normalsTextureId !== undefined && cfg.normalsTextureId !== null) { normalsTexture = this._textures[cfg.normalsTextureId]; if (!normalsTexture) { this.error(`[createTextureSet] Texture not found: ${cfg.normalsTextureId} - ensure that you create it first with createTexture()`); return; } } else { normalsTexture = this._textures[DEFAULT_NORMALS_TEXTURE_ID]; } let emissiveTexture; if (cfg.emissiveTextureId !== undefined && cfg.emissiveTextureId !== null) { emissiveTexture = this._textures[cfg.emissiveTextureId]; if (!emissiveTexture) { this.error(`[createTextureSet] Texture not found: ${cfg.emissiveTextureId} - ensure that you create it first with createTexture()`); return; } } else { emissiveTexture = this._textures[DEFAULT_EMISSIVE_TEXTURE_ID]; } let occlusionTexture; if (cfg.occlusionTextureId !== undefined && cfg.occlusionTextureId !== null) { occlusionTexture = this._textures[cfg.occlusionTextureId]; if (!occlusionTexture) { this.error(`[createTextureSet] Texture not found: ${cfg.occlusionTextureId} - ensure that you create it first with createTexture()`); return; } } else { occlusionTexture = this._textures[DEFAULT_OCCLUSION_TEXTURE_ID]; } const textureSet = new SceneModelTextureSet({ id: textureSetId, model: this, colorTexture, alphaCutoff: cfg.alphaCutoff, metallicRoughnessTexture, normalsTexture, emissiveTexture, occlusionTexture }); this._textureSets[textureSetId] = textureSet; return textureSet; } /** * Creates a new {@link SceneModelTransform} within this SceneModel. * * * Stores the new {@link SceneModelTransform} in {@link SceneModel#transforms}. * * Can be connected into hierarchies * * Each {@link SceneModelTransform} can be used by unlimited {@link SceneModelMesh}es * * @param {*} cfg Transform creation parameters. * @param {String} cfg.id Mandatory ID for the new transform. Must not clash with any existing components within the {@link Scene}. * @param {String} [cfg.parentTransformId] ID of a parent transform, previously created with {@link SceneModel#createTextureSet}. * @param {Number[]} [cfg.position=[0,0,0]] Local 3D position of the mesh. Overridden by ````transformId````. * @param {Number[]} [cfg.scale=[1,1,1]] Scale of the transform. * @param {Number[]} [cfg.rotation=[0,0,0]] Rotation of the transform as Euler angles given in degrees, for each of the X, Y and Z axis. * @param {Number[]} [cfg.matrix=[1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1]] Modelling transform matrix. Overrides the ````position````, ````scale```` and ````rotation```` parameters. * @returns {SceneModelTransform} The new transform. */ createTransform(cfg) { if (cfg.id === undefined || cfg.id === null) { this.error("[createTransform] SceneModel.createTransform() config missing: id"); return; } if (this._transforms[cfg.id]) { this.error(`[createTransform] SceneModel already has a transform with this ID: ${cfg.id}`); return; } let parentTransform; if (cfg.parentTransformId) { parentTransform = this._transforms[cfg.parentTransformId]; if (!parentTransform) { this.error("[createTransform] SceneModel.createTransform() config missing: id"); return; } } const transform = new SceneModelTransform({ id: cfg.id, model: this, parent: parentTransform, matrix: cfg.matrix, position: cfg.position, scale: cfg.scale, rotation: cfg.rotation, quaternion: cfg.quaternion }); this._transforms[transform.id] = transform; return transform; } /** * Creates a new {@link SceneModelMesh} within this SceneModel. * * * It prepares and saves data for a SceneModelMesh {@link SceneModel#meshes} creation. SceneModelMesh will be created only once the SceneModelEntity (which references this particular SceneModelMesh) will be created. * * The SceneModelMesh can either define its own geometry or share it with other SceneModelMeshes. To define own geometry, provide the * various geometry arrays to this method. To share a geometry, provide the ID of a geometry created earlier * with {@link SceneModel#createGeometry}. * * If you accompany the arrays with an ````origin````, then ````createMesh()```` will assume * that the geometry ````positions```` are in relative-to-center (RTC) coordinates, with ````origin```` being the * origin of their RTC coordinate system. * * @param {object} cfg Object properties. * @param {String} cfg.id Mandatory ID for the new mesh. Must not clash with any existing components within the {@link Scene}. * @param {String|Number} [cfg.textureSetId] ID of a {@link SceneModelTextureSet} previously created with {@link SceneModel#createTextureSet}. * @param {String|Number} [cfg.transformId] ID of a {@link SceneModelTransform} to instance, previously created with {@link SceneModel#createTransform}. Overrides all other transform parameters given to this method. * @param {String|Number} [cfg.geometryId] ID of a geometry to instance, previously created with {@link SceneModel#createGeometry}. Overrides all other geometry parameters given to this method. * @param {String} cfg.primitive The primitive type. Accepted values are 'points', 'lines', 'triangles', 'solid' and 'surface'. * @param {Number[]} [cfg.positions] Flat array of uncompressed 3D vertex positions positions. Required for all primitive types. Overridden by ````positionsCompressed````. * @param {Number[]} [cfg.positionsCompressed] Flat array of quantized 3D vertex positions. Overrides ````positions````, and must be accompanied by ````positionsDecodeMatrix````. * @param {Number[]} [cfg.positionsDecodeMatrix] A 4x4 matrix for decompressing ````positionsCompressed````. Must be accompanied by ````positionsCompressed````. * @param {Number[]} [cfg.normals] Flat array of normal vectors. Only used with "triangles", "solid" and "surface" primitives. When no normals are given, the geometry will be flat shaded using auto-generated face-aligned normals. * @param {Number[]} [cfg.normalsCompressed] Flat array of oct-encoded normal vectors. Overrides ````normals````. Only used with "triangles", "solid" and "surface" primitives. When no normals are given, the geometry will be flat shaded using auto-generated face-aligned normals. * @param {Number[]} [cfg.colors] Flat array of uncompressed RGBA vertex colors, as float values in range ````[0..1]````. Ignored when ````geometryId```` is given. Overridden by ````color```` and ````colorsCompressed````. * @param {Number[]} [cfg.colorsCompressed] Flat array of compressed RGBA vertex colors, as unsigned short integers in range ````[0..255]````. Ignored when ````geometryId```` is given. Overrides ````colors```` and is overridden by ````color````. * @param {Number[]} [cfg.uv] Flat array of uncompressed vertex UV coordinates. Only used with "triangles", "solid" and "surface" primitives. Required for textured rendering. * @param {Number[]} [cfg.uvCompressed] Flat array of compressed vertex UV coordinates. Only used with "triangles", "solid" and "surface" primitives. Overrides ````uv````. Must be accompanied by ````uvDecodeMatrix````. Only used with "triangles", "solid" and "surface" primitives. Required for textured rendering. * @param {Number[]} [cfg.uvDecodeMatrix] A 3x3 matrix for decompressing ````uvCompressed````. * @param {Number[]} [cfg.indices] Array of primitive connectivity indices. Not required for `points` primitives. * @param {Number[]} [cfg.edgeIndices] Array of edge line indices. Used only with 'triangles', 'solid' and 'surface' primitives. Automatically generated internally if not supplied, using the optional ````edgeThreshold```` given to the ````SceneModel```` constructor. * @param {Number[]} [cfg.origin] Optional geometry origin, relative to {@link SceneModel#origin}. When this is given, then ````positions```` are assumed to be relative to this. * @param {Number[]} [cfg.position=[0,0,0]] Local 3D position of the mesh. Overridden by ````transformId````. * @param {Number[]} [cfg.scale=[1,1,1]] Scale of the mesh. Overridden by ````transformId````. * @param {Number[]} [cfg.rotation=[0,0,0]] Rotation of the mesh as Euler angles given in degrees, for each of the X, Y and Z axis. Overridden by ````transformId````. * @param {Number[]} [cfg.quaternion] Rotation of the mesh as a quaternion. Overridden by ````rotation````. * @param {Number[]} [cfg.matrix=[1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1]] Mesh modelling transform matrix. Overrides the ````position````, ````scale```` and ````rotation```` parameters. Also overridden by ````transformId````. * @param {Number[]} [cfg.color=[1,1,1]] RGB color in range ````[0..1, 0..1, 0..1]````. Overridden by texture set ````colorTexture````. Overrides ````colors```` and ````colorsCompressed````. * @param {Number} [cfg.opacity=1] Opacity in range ````[0..1]````. Overridden by texture set ````colorTexture````. * @param {Number} [cfg.metallic=0] Metallic factor in range ````[0..1]````. Overridden by texture set ````metallicRoughnessTexture````. * @param {Number} [cfg.roughness=1] Roughness factor in range ````[0..1]````. Overridden by texture set ````metallicRoughnessTexture````. * @returns {SceneModelMesh} The new mesh. */ createMesh(cfg) { if (cfg.id === undefined || cfg.id === null) { this.error("[createMesh] SceneModel.createMesh() config missing: id"); return false; } if (this._meshes[cfg.id]) { this.error(`[createMesh] SceneModel already has a mesh with this ID: ${cfg.id}`); return false; } const instancing = (cfg.geometryId !== undefined); const batching = !instancing; if (batching) { // Batched geometry if (cfg.primitive === undefined || cfg.primitive === null) { cfg.primitive = "triangles"; } if (cfg.primitive !== "points" && cfg.primitive !== "lines" && cfg.primitive !== "triangles" && cfg.primitive !== "solid" && cfg.primitive !== "surface") { this.error(`Unsupported value for 'primitive': '${primitive}' ('geometryId' is absent) - supported values are 'points', 'lines', 'triangles', 'solid' and 'surface'.`); return false; } if (!cfg.positions && !cfg.positionsCompressed && !cfg.buckets) { this.error("Param expected: 'positions', 'positionsCompressed' or `buckets` ('geometryId' is absent)"); return false; } if (cfg.positions && (cfg.positionsDecodeMatrix || cfg.positionsDecodeBoundary)) { this.error("Illegal params: 'positions' not expected with 'positionsDecodeMatrix'/'positionsDecodeBoundary' ('geometryId' is absent)"); return false; } if (cfg.positionsCompressed && !cfg.positionsDecodeMatrix && !cfg.positionsDecodeBoundary) { this.error("Param expected: 'positionsCompressed' should be accompanied by 'positionsDecodeMatrix'/'positionsDecodeBoundary' ('geometryId' is absent)"); return false; } if (cfg.uvCompressed && !cfg.uvDecodeMatrix) { this.error("Param expected: 'uvCompressed' should be accompanied by `uvDecodeMatrix` ('geometryId' is absent)"); return false; } if (!cfg.buckets && !cfg.indices && (cfg.primitive === "triangles" || cfg.primitive === "solid" || cfg.primitive === "surface")) { const numPositions = (cfg.positions || cfg.positionsCompressed).length / 3; cfg.indices = this._createDefaultIndices(numPositions); } if (!cfg.buckets && !cfg.indices && cfg.primitive !== "points") { cfg.indices = this._createDefaultIndices(numIndices); this.error(`Param expected: indices (required for '${cfg.primitive}' primitive type)`); return false; } if ((cfg.matrix || cfg.position || cfg.rotation || cfg.scale) && (cfg.positionsCompressed || cfg.positionsDecodeBoundary)) { this.error("Unexpected params: 'matrix', 'rotation', 'scale', 'position' not allowed with 'positionsCompressed'"); return false; } const useDTX = (!!this._dtxEnabled && (cfg.primitive === "triangles" || cfg.primitive === "solid" || cfg.primitive === "surface")) && (!cfg.textureSetId); cfg.origin = cfg.origin ? math.addVec3(this._origin, cfg.origin, math.vec3()) : this._origin; // MATRIX - optional for batching if (cfg.matrix) { cfg.meshMatrix = cfg.matrix; } else if (cfg.scale || cfg.rotation || cfg.position || cfg.quaternion) { const scale = cfg.scale || DEFAULT_SCALE; const position = cfg.position || DEFAULT_POSITION; if (cfg.rotation) { math.eulerToQuaternion(cfg.rotation, "XYZ", tempQuaternion); cfg.meshMatrix = math.composeMat4(position, tempQuaternion, scale, math.mat4()); } else { cfg.meshMatrix = math.composeMat4(position, cfg.quaternion || DEFAULT_QUATERNION, scale, math.mat4()); } } if (cfg.positionsDecodeBoundary) { cfg.positionsDecodeMatrix = createPositionsDecodeMatrix(cfg.positionsDecodeBoundary, math.mat4()); } if (useDTX) { // DTX cfg.type = DTX; // NPR cfg.color = (cfg.color) ? new Uint8Array([Math.floor(cfg.color[0] * 255), Math.floor(cfg.color[1] * 255), Math.floor(cfg.color[2] * 255)]) : defaultCompressedColor; cfg.opacity = (cfg.opacity !== undefined && cfg.opacity !== null) ? Math.floor(cfg.opacity * 255) : 255; // RTC if (cfg.positions) { const rtcCenter = math.vec3(); const rtcPositions = []; const rtcNeeded = worldToRTCPositions(cfg.positions, rtcPositions, rtcCenter); if (rtcNeeded) { cfg.positions = rtcPositions; cfg.origin = math.addVec3(cfg.origin, rtcCenter, rtcCenter); } } // COMPRESSION if (cfg.positions) { const aabb = math.collapseAABB3(); cfg.positionsDecodeMatrix = math.mat4(); math.expandAABB3Points3(aabb, cfg.positions); cfg.positionsCompressed = quantizePositions(cfg.positions, aabb, cfg.positionsDecodeMatrix); cfg.aabb = aabb; } else if (cfg.positionsCompressed) { const aabb = math.collapseAABB3(); math.expandAABB3Points3(aabb, cfg.positionsCompressed); geometryCompressionUtils.decompressAABB(aabb, cfg.positionsDecodeMatrix); cfg.aabb = aabb; } if (cfg.buckets) { const aabb = math.collapseAABB3(); for (let i = 0, len = cfg.buckets.length; i < len; i++) { const bucket = cfg.buckets[i]; if (bucket.positions) { math.expandAABB3Points3(aabb, bucket.positions); } else if (bucket.positionsCompressed) { math.expandAABB3Points3(aabb, bucket.positionsCompressed); } } if (cfg.positionsDecodeMatrix) { geometryCompressionUtils.decompressAABB(aabb, cfg.positionsDecodeMatrix); } cfg.aabb = aabb; } if (cfg.meshMatrix) { math.AABB3ToOBB3(cfg.aabb, tempOBB3); math.transformOBB3(cfg.meshMatrix, tempOBB3); math.OBB3ToAABB3(tempOBB3, cfg.aabb); } // EDGES if (!cfg.buckets && !cfg.edgeIndices && (cfg.primitive === "triangles" || cfg.primitive === "solid" || cfg.primitive === "surface")) { if (cfg.positions) { // Faster cfg.edgeIndices = buildEdgeIndices(cfg.positions, cfg.indices, null, 2.0); } else { cfg.edgeIndices = buildEdgeIndices(cfg.positionsCompressed, cfg.indices, cfg.positionsDecodeMatrix, 2.0); } } // BUCKETING if (!cfg.buckets) { cfg.buckets = createDTXBuckets(cfg, this._enableVertexWelding && this._enableIndexBucketing); } } else { // VBO cfg.type = VBO_BATCHED; // PBR cfg.color = (cfg.color) ? new Uint8Array([Math.floor(cfg.color[0] * 255), Math.floor(cfg.color[1] * 255), Math.floor(cfg.color[2] * 255)]) : [255, 255, 255]; cfg.opacity = (cfg.opacity !== undefined && cfg.opacity !== null) ? Math.floor(cfg.opacity * 255) : 255; cfg.metallic = (cfg.metallic !== undefined && cfg.metallic !== null) ? Math.floor(cfg.metallic * 255) : 0; cfg.roughness = (cfg.roughness !== undefined && cfg.roughness !== null) ? Math.floor(cfg.roughness * 255) : 255; // RTC if (cfg.positions) { const rtcPositions = []; const rtcNeeded = worldToRTCPositions(cfg.positions, rtcPositions, tempVec3a$d); if (rtcNeeded) { cfg.positions = rtcPositions; cfg.origin = math.addVec3(cfg.origin, tempVec3a$d, math.vec3()); } } if (cfg.positions) { const aabb = math.collapseAABB3(); if (cfg.meshMatrix) { math.transformPositions3(cfg.meshMatrix, cfg.positions, cfg.positions); cfg.meshMatrix = null; // Positions now baked, don't need any more } math.expandAABB3Points3(aabb, cfg.positions); cfg.aabb = aabb; } else { const aabb = math.collapseAABB3(); math.expandAABB3Points3(aabb, cfg.positionsCompressed); geometryCompressionUtils.decompressAABB(aabb, cfg.positionsDecodeMatrix); cfg.aabb = aabb; } if (cfg.meshMatrix) { math.AABB3ToOBB3(cfg.aabb, tempOBB3); math.transformOBB3(cfg.meshMatrix, tempOBB3); math.OBB3ToAABB3(tempOBB3, cfg.aabb); } // EDGES if (!cfg.buckets && !cfg.edgeIndices && (cfg.primitive === "triangles" || cfg.primitive === "solid" || cfg.primitive === "surface")) { if (cfg.positions) { cfg.edgeIndices = buildEdgeIndices(cfg.positions, cfg.indices, null, 2.0); } else { cfg.edgeIndices = buildEdgeIndices(cfg.positionsCompressed, cfg.indices, cfg.positionsDecodeMatrix, 2.0); } } // TEXTURE // cfg.textureSetId = cfg.textureSetId || DEFAULT_TEXTURE_SET_ID; if (cfg.textureSetId) { cfg.textureSet = this._textureSets[cfg.textureSetId]; if (!cfg.textureSet) { this.error(`[createMesh] Texture set not found: ${cfg.textureSetId} - ensure that you create it first with createTextureSet()`); return false; } } } } else { // INSTANCING if (cfg.positions || cfg.positionsCompressed || cfg.indices || cfg.edgeIndices || cfg.normals || cfg.normalsCompressed || cfg.uv || cfg.uvCompressed || cfg.positionsDecodeMatrix) { this.error(`Mesh geometry parameters not expected when instancing a geometry (not expected: positions, positionsCompressed, indices, edgeIndices, normals, normalsCompressed, uv, uvCompressed, positionsDecodeMatrix)`); return false; } cfg.geometry = this._geometries[cfg.geometryId]; if (!cfg.geometry) { this.error(`[createMesh] Geometry not found: ${cfg.geometryId} - ensure that you create it first with createGeometry()`); return false; } cfg.origin = cfg.origin ? math.addVec3(this._origin, cfg.origin, math.vec3()) : this._origin; cfg.positionsDecodeMatrix = cfg.geometry.positionsDecodeMatrix; if (cfg.transformId) { // TRANSFORM cfg.transform = this._transforms[cfg.transformId]; if (!cfg.transform) { this.error(`[createMesh] Transform not found: ${cfg.transformId} - ensure that you create it first with createTransform()`); return false; } cfg.aabb = cfg.geometry.aabb; } else { // MATRIX if (cfg.matrix) { cfg.meshMatrix = cfg.matrix; } else if (cfg.scale || cfg.rotation || cfg.position || cfg.quaternion) { const scale = cfg.scale || DEFAULT_SCALE; const position = cfg.position || DEFAULT_POSITION; if (cfg.rotation) { math.eulerToQuaternion(cfg.rotation, "XYZ", tempQuaternion); cfg.meshMatrix = math.composeMat4(position, tempQuaternion, scale, math.mat4()); } else { cfg.meshMatrix = math.composeMat4(position, cfg.quaternion || DEFAULT_QUATERNION, scale, math.mat4()); } } math.AABB3ToOBB3(cfg.geometry.aabb, tempOBB3); math.transformOBB3(cfg.meshMatrix, tempOBB3); cfg.aabb = math.OBB3ToAABB3(tempOBB3, math.AABB3()); } const useDTX = (!!this._dtxEnabled && (cfg.geometry.primitive === "triangles" || cfg.geometry.primitive === "solid" || cfg.geometry.primitive === "surface")) && (!cfg.textureSetId); if (useDTX) { // DTX cfg.type = DTX; // NPR cfg.color = (cfg.color) ? new Uint8Array([Math.floor(cfg.color[0] * 255), Math.floor(cfg.color[1] * 255), Math.floor(cfg.color[2] * 255)]) : defaultCompressedColor; cfg.opacity = (cfg.opacity !== undefined && cfg.opacity !== null) ? Math.floor(cfg.opacity * 255) : 255; // BUCKETING - lazy generated, reused let buckets = this._dtxBuckets[cfg.geometryId]; if (!buckets) { buckets = createDTXBuckets(cfg.geometry, this._enableVertexWelding, this._enableIndexBucketing); this._dtxBuckets[cfg.geometryId] = buckets; } cfg.buckets = buckets; } else { // VBO cfg.type = VBO_INSTANCED; // PBR cfg.color = (cfg.color) ? new Uint8Array([Math.floor(cfg.color[0] * 255), Math.floor(cfg.color[1] * 255), Math.floor(cfg.color[2] * 255)]) : defaultCompressedColor; cfg.opacity = (cfg.opacity !== undefined && cfg.opacity !== null) ? Math.floor(cfg.opacity * 255) : 255; cfg.metallic = (cfg.metallic !== undefined && cfg.metallic !== null) ? Math.floor(cfg.metallic * 255) : 0; cfg.roughness = (cfg.roughness !== undefined && cfg.roughness !== null) ? Math.floor(cfg.roughness * 255) : 255; // TEXTURE if (cfg.textureSetId) { cfg.textureSet = this._textureSets[cfg.textureSetId]; // if (!cfg.textureSet) { // this.error(`[createMesh] Texture set not found: ${cfg.textureSetId} - ensure that you create it first with createTextureSet()`); // return false; // } } } } cfg.numPrimitives = this._getNumPrimitives(cfg); return this._createMesh(cfg); } _createDefaultIndices(numIndices) { const indices = []; for (let i = 0; i < numIndices; i++) { indices.push(i); } return indices; } _createMesh(cfg) { const mesh = new SceneModelMesh(this, cfg.id, cfg.color, cfg.opacity, cfg.transform, cfg.textureSet); mesh.pickId = this.scene._renderer.getPickID(mesh); const pickId = mesh.pickId; const a = pickId >> 24 & 0xFF; const b = pickId >> 16 & 0xFF; const g = pickId >> 8 & 0xFF; const r = pickId & 0xFF; cfg.pickColor = new Uint8Array([r, g, b, a]); // Quantized pick color cfg.solid = (cfg.primitive === "solid"); mesh.origin = math.vec3(cfg.origin); switch (cfg.type) { case DTX: mesh.layer = this._getDTXLayer(cfg); mesh.aabb = cfg.aabb; break; case VBO_BATCHED: mesh.layer = this._getVBOBatchingLayer(cfg); mesh.aabb = cfg.aabb; break; case VBO_INSTANCED: mesh.layer = this._getVBOInstancingLayer(cfg); mesh.aabb = cfg.aabb; break; } if (cfg.transform) { cfg.meshMatrix = cfg.transform.worldMatrix; } mesh.portionId = mesh.layer.createPortion(mesh, cfg); mesh.numPrimitives = cfg.numPrimitives; this._meshes[cfg.id] = mesh; this._unusedMeshes[cfg.id] = mesh; this._meshList.push(mesh); return mesh; } _getNumPrimitives(cfg) { let countIndices = 0; const primitive = cfg.geometry ? cfg.geometry.primitive : cfg.primitive; switch (primitive) { case "triangles": case "solid": case "surface": switch (cfg.type) { case DTX: for (let i = 0, len = cfg.buckets.length; i < len; i++) { countIndices += cfg.buckets[i].indices.length; } break; case VBO_BATCHED: countIndices += cfg.indices.length; break; case VBO_INSTANCED: countIndices += cfg.geometry.indices.length; break; } return Math.round(countIndices / 3); case "points": switch (cfg.type) { case DTX: for (let i = 0, len = cfg.buckets.length; i < len; i++) { countIndices += cfg.buckets[i].positionsCompressed.length; } break; case VBO_BATCHED: countIndices += cfg.positions ? cfg.positions.length : cfg.positionsCompressed.length; break; case VBO_INSTANCED: const geometry = cfg.geometry; countIndices += geometry.positions ? geometry.positions.length : geometry.positionsCompressed.length; break; } return Math.round(countIndices); case "lines": case "line-strip": switch (cfg.type) { case DTX: for (let i = 0, len = cfg.buckets.length; i < len; i++) { countIndices += cfg.buckets[i].indices.length; } break; case VBO_BATCHED: countIndices += cfg.indices.length; break; case VBO_INSTANCED: countIndices += cfg.geometry.indices.length; break; } return Math.round(countIndices / 2); } return 0; } _getDTXLayer(cfg) { const origin = cfg.origin; const primitive = cfg.geometry ? cfg.geometry.primitive : cfg.primitive; const layerId = `.${primitive}.${Math.round(origin[0])}.${Math.round(origin[1])}.${Math.round(origin[2])}`; let dtxLayer = this._dtxLayers[layerId]; if (dtxLayer) { if (!dtxLayer.canCreatePortion(cfg)) { // dtxLayer.finalize(); delete this._dtxLayers[layerId]; dtxLayer = null; } else { return dtxLayer; } } switch (primitive) { case "triangles": case "solid": case "surface": dtxLayer = new DTXTrianglesLayer(this, {layerIndex: 0, origin, primitive}); // layerIndex is set in #finalize() break; case "lines": dtxLayer = new DTXLinesLayer(this, {layerIndex: 0, origin, primitive}); // layerIndex is set in #finalize() break; default: return; } this._dtxLayers[layerId] = dtxLayer; this.layerList.push(dtxLayer); this._layersToFinalize.push(dtxLayer); return dtxLayer; } _getVBOBatchingLayer(cfg) { const model = this; const origin = cfg.origin; cfg.renderLayer || 0; const positionsDecodeHash = cfg.positionsDecodeMatrix || cfg.positionsDecodeBoundary ? this._createHashStringFromMatrix(cfg.positionsDecodeMatrix || cfg.positionsDecodeBoundary) : "-"; const textureSetId = cfg.textureSetId || "-"; const layerId = `${Math.round(origin[0])}.${Math.round(origin[1])}.${Math.round(origin[2])}.${cfg.primitive}.${positionsDecodeHash}.${textureSetId}`; let vboBatchingLayer = this._vboBatchingLayers[layerId]; if (vboBatchingLayer) { return vboBatchingLayer; } let textureSet = cfg.textureSet; while (!vboBatchingLayer) { switch (cfg.primitive) { case "triangles": // console.info(`[SceneModel ${this.id}]: creating TrianglesBatchingLayer`); vboBatchingLayer = new VBOBatchingTrianglesLayer({ model, textureSet, layerIndex: 0, // This is set in #finalize() scratchMemory: this._vboBatchingLayerScratchMemory, positionsDecodeMatrix: cfg.positionsDecodeMatrix, // Can be undefined uvDecodeMatrix: cfg.uvDecodeMatrix, // Can be undefined origin, maxGeometryBatchSize: this._maxGeometryBatchSize, solid: (cfg.primitive === "solid"), autoNormals: true, primitive: cfg.primitive }); break; case "solid": // console.info(`[SceneModel ${this.id}]: creating TrianglesBatchingLayer`); vboBatchingLayer = new VBOBatchingTrianglesLayer({ model, textureSet, layerIndex: 0, // This is set in #finalize() scratchMemory: this._vboBatchingLayerScratchMemory, positionsDecodeMatrix: cfg.positionsDecodeMatrix, // Can be undefined uvDecodeMatrix: cfg.uvDecodeMatrix, // Can be undefined origin, maxGeometryBatchSize: this._maxGeometryBatchSize, solid: (cfg.primitive === "solid"), autoNormals: true, primitive: cfg.primitive }); break; case "surface": // console.info(`[SceneModel ${this.id}]: creating TrianglesBatchingLayer`); vboBatchingLayer = new VBOBatchingTrianglesLayer({ model, textureSet, layerIndex: 0, // This is set in #finalize() scratchMemory: this._vboBatchingLayerScratchMemory, positionsDecodeMatrix: cfg.positionsDecodeMatrix, // Can be undefined uvDecodeMatrix: cfg.uvDecodeMatrix, // Can be undefined origin, maxGeometryBatchSize: this._maxGeometryBatchSize, solid: (cfg.primitive === "solid"), autoNormals: true, primitive: cfg.primitive }); break; case "lines": // console.info(`[SceneModel ${this.id}]: creating VBOBatchingLinesLayer`); vboBatchingLayer = new VBOBatchingLinesLayer({ model, layerIndex: 0, // This is set in #finalize() scratchMemory: this._vboBatchingLayerScratchMemory, positionsDecodeMatrix: cfg.positionsDecodeMatrix, // Can be undefined uvDecodeMatrix: cfg.uvDecodeMatrix, // Can be undefined origin, maxGeometryBatchSize: this._maxGeometryBatchSize, primitive: cfg.primitive }); break; case "points": // console.info(`[SceneModel ${this.id}]: creating VBOBatchingPointsLayer`); vboBatchingLayer = new VBOBatchingPointsLayer({ model, layerIndex: 0, // This is set in #finalize() scratchMemory: this._vboBatchingLayerScratchMemory, positionsDecodeMatrix: cfg.positionsDecodeMatrix, // Can be undefined uvDecodeMatrix: cfg.uvDecodeMatrix, // Can be undefined origin, maxGeometryBatchSize: this._maxGeometryBatchSize, primitive: cfg.primitive }); break; } const lenPositions = cfg.positionsCompressed ? cfg.positionsCompressed.length : cfg.positions.length; const canCreatePortion = (cfg.primitive === "points") ? vboBatchingLayer.canCreatePortion(lenPositions) : vboBatchingLayer.canCreatePortion(lenPositions, cfg.indices.length); if (!canCreatePortion) { vboBatchingLayer.finalize(); delete this._vboBatchingLayers[layerId]; vboBatchingLayer = null; } } this._vboBatchingLayers[layerId] = vboBatchingLayer; this.layerList.push(vboBatchingLayer); this._layersToFinalize.push(vboBatchingLayer); return vboBatchingLayer; } _createHashStringFromMatrix(matrix) { const matrixString = matrix.join(''); let hash = 0; for (let i = 0; i < matrixString.length; i++) { const char = matrixString.charCodeAt(i); hash = (hash << 5) - hash + char; hash |= 0; // Convert to 32-bit integer } const hashString = (hash >>> 0).toString(16); return hashString; } _getVBOInstancingLayer(cfg) { const model = this; const origin = cfg.origin; const textureSetId = cfg.textureSetId || "-"; const geometryId = cfg.geometryId; const layerId = `${Math.round(origin[0])}.${Math.round(origin[1])}.${Math.round(origin[2])}.${textureSetId}.${geometryId}`; let vboInstancingLayer = this._vboInstancingLayers[layerId]; if (vboInstancingLayer) { return vboInstancingLayer; } let textureSet = cfg.textureSet; const geometry = cfg.geometry; while (!vboInstancingLayer) { switch (geometry.primitive) { case "triangles": // console.info(`[SceneModel ${this.id}]: creating TrianglesInstancingLayer`); vboInstancingLayer = new VBOInstancingTrianglesLayer({ model, textureSet, geometry, origin, layerIndex: 0, solid: false }); break; case "solid": // console.info(`[SceneModel ${this.id}]: creating TrianglesInstancingLayer`); vboInstancingLayer = new VBOInstancingTrianglesLayer({ model, textureSet, geometry, origin, layerIndex: 0, solid: true }); break; case "surface": // console.info(`[SceneModel ${this.id}]: creating TrianglesInstancingLayer`); vboInstancingLayer = new VBOInstancingTrianglesLayer({ model, textureSet, geometry, origin, layerIndex: 0, solid: false }); break; case "lines": // console.info(`[SceneModel ${this.id}]: creating VBOInstancingLinesLayer`); vboInstancingLayer = new VBOInstancingLinesLayer({ model, textureSet, geometry, origin, layerIndex: 0 }); break; case "points": // console.info(`[SceneModel ${this.id}]: creating PointsInstancingLayer`); vboInstancingLayer = new VBOInstancingPointsLayer({ model, textureSet, geometry, origin, layerIndex: 0 }); break; } // const lenPositions = geometry.positionsCompressed.length; // if (!vboInstancingLayer.canCreatePortion(lenPositions, geometry.indices.length)) { // FIXME: indices should be optional // vboInstancingLayer.finalize(); // delete this._vboInstancingLayers[layerId]; // vboInstancingLayer = null; // } } this._vboInstancingLayers[layerId] = vboInstancingLayer; this.layerList.push(vboInstancingLayer); this._layersToFinalize.push(vboInstancingLayer); return vboInstancingLayer; } /** * Creates a {@link SceneModelEntity} within this SceneModel. * * * Gives the SceneModelEntity one or more {@link SceneModelMesh}es previously created with * {@link SceneModel#createMesh}. A SceneModelMesh can only belong to one SceneModelEntity, so you'll get an * error if you try to reuse a mesh among multiple SceneModelEntitys. * * The SceneModelEntity can have a {@link SceneModelTextureSet}, previously created with * {@link SceneModel#createTextureSet}. A SceneModelTextureSet can belong to multiple SceneModelEntitys. * * The SceneModelEntity can have a geometry, previously created with * {@link SceneModel#createTextureSet}. A geometry is a "virtual component" and can belong to multiple SceneModelEntitys. * * @param {Object} cfg SceneModelEntity configuration. * @param {String} cfg.id Optional ID for the new SceneModelEntity. Must not clash with any existing components within the {@link Scene}. * @param {String[]} cfg.meshIds IDs of one or more meshes created previously with {@link SceneModel@createMesh}. * @param {Boolean} [cfg.isObject] Set ````true```` if the {@link SceneModelEntity} represents an object, in which case it will be registered by {@link SceneModelEntity#id} in {@link Scene#objects} and can also have a corresponding {@link MetaObject} with matching {@link MetaObject#id}, registered by that ID in {@link MetaScene#metaObjects}. * @param {Boolean} [cfg.visible=true] Indicates if the SceneModelEntity is initially visible. * @param {Boolean} [cfg.culled=false] Indicates if the SceneModelEntity is initially culled from view. * @param {Boolean} [cfg.pickable=true] Indicates if the SceneModelEntity is initially pickable. * @param {Boolean} [cfg.clippable=true] Indicates if the SceneModelEntity is initially clippable. * @param {Boolean} [cfg.collidable=true] Indicates if the SceneModelEntity is initially included in boundary calculations. * @param {Boolean} [cfg.castsShadow=true] Indicates if the SceneModelEntity initially casts shadows. * @param {Boolean} [cfg.receivesShadow=true] Indicates if the SceneModelEntity initially receives shadows. * @param {Boolean} [cfg.xrayed=false] Indicates if the SceneModelEntity is initially xrayed. XRayed appearance is configured by {@link SceneModel#xrayMaterial}. * @param {Boolean} [cfg.highlighted=false] Indicates if the SceneModelEntity is initially highlighted. Highlighted appearance is configured by {@link SceneModel#highlightMaterial}. * @param {Boolean} [cfg.selected=false] Indicates if the SceneModelEntity is initially selected. Selected appearance is configured by {@link SceneModel#selectedMaterial}. * @param {Boolean} [cfg.edges=false] Indicates if the SceneModelEntity's edges are initially emphasized. Edges appearance is configured by {@link SceneModel#edgeMaterial}. * @returns {SceneModelEntity} The new SceneModelEntity. */ createEntity(cfg) { if (cfg.id === undefined) { cfg.id = math.createUUID(); } else if (this.scene.components[cfg.id]) { this.error(`Scene already has a Component with this ID: ${cfg.id} - will assign random ID`); cfg.id = math.createUUID(); } if (cfg.meshIds === undefined) { this.error("Config missing: meshIds"); return; } let flags = 0; if (this._visible && cfg.visible !== false) { flags = flags | ENTITY_FLAGS.VISIBLE; } if (this._pickable && cfg.pickable !== false) { flags = flags | ENTITY_FLAGS.PICKABLE; } if (this._culled && cfg.culled !== false) { flags = flags | ENTITY_FLAGS.CULLED; } if (this._clippable && cfg.clippable !== false) { flags = flags | ENTITY_FLAGS.CLIPPABLE; } if (this._collidable && cfg.collidable !== false) { flags = flags | ENTITY_FLAGS.COLLIDABLE; } if (this._edges && cfg.edges !== false) { flags = flags | ENTITY_FLAGS.EDGES; } if (this._xrayed && cfg.xrayed !== false) { flags = flags | ENTITY_FLAGS.XRAYED; } if (this._highlighted && cfg.highlighted !== false) { flags = flags | ENTITY_FLAGS.HIGHLIGHTED; } if (this._selected && cfg.selected !== false) { flags = flags | ENTITY_FLAGS.SELECTED; } cfg.flags = flags; return this._createEntity(cfg); } _createEntity(cfg) { let meshes = []; for (let i = 0, len = cfg.meshIds.length; i < len; i++) { const meshId = cfg.meshIds[i]; let mesh = this._meshes[meshId]; // Trying to get already created mesh if (!mesh) { // Checks if there is already created mesh for this meshId this.error(`Mesh with this ID not found: "${meshId}" - ignoring this mesh`); // There is no such cfg continue; } if (mesh.parent) { this.error(`Mesh with ID "${meshId}" already belongs to object with ID "${mesh.parent.id}" - ignoring this mesh`); continue; } meshes.push(mesh); delete this._unusedMeshes[meshId]; } const lodCullable = true; const entity = new SceneModelEntity( this, cfg.isObject, cfg.id, meshes, cfg.flags, lodCullable); // Internally sets SceneModelEntity#parent to this SceneModel this._entityList.push(entity); this._entities[cfg.id] = entity; this._entitiesToFinalize.push(entity); this.numEntities++; return entity; } /** * Pre-renders all meshes that have been added, even if the SceneModel has not bee finalized yet. * This is use for progressively showing the SceneModel while it is being loaded or constructed. * @returns {boolean} */ preFinalize() { if (this.destroyed) { return false; } if (this._layersToFinalize.length === 0) { return false; } this._createDummyEntityForUnusedMeshes(); for (let i = 0, len = this._layersToFinalize.length; i < len; i++) { const layer = this._layersToFinalize[i]; layer.finalize(); } this._vboBatchingLayers = {}; this._vboInstancingLayers = {}; this._dtxLayers = {}; this._layersToFinalize = []; for (let i = 0, len = this._entitiesToFinalize.length; i < len; i++) { const entity = this._entitiesToFinalize[i]; entity._finalize(); } for (let i = 0, len = this._entitiesToFinalize.length; i < len; i++) { const entity = this._entitiesToFinalize[i]; entity._finalize2(); } this._entitiesToFinalize = []; this.scene._aabbDirty = true; this._viewMatrixDirty = true; this._matrixDirty = true; this._aabbDirty = true; this._setWorldMatrixDirty(); this._sceneModelDirty(); this.position = this._position; // Sort layers to reduce WebGL shader switching when rendering them this.layerList.sort((a, b) => { if (a.sortId < b.sortId) { return -1; } if (a.sortId > b.sortId) { return 1; } return 0; }); for (let i = 0, len = this.layerList.length; i < len; i++) { const layer = this.layerList[i]; layer.layerIndex = i; } this.glRedraw(); this._layersFinalized = true; } /** * Finalizes this SceneModel. * * Once finalized, you can't add anything more to this SceneModel. */ finalize() { if (this.destroyed) { return; } this.preFinalize(); this._geometries = {}; this._dtxBuckets = {}; this._textures = {}; this._textureSets = {}; } /** @private */ stateSortCompare(drawable1, drawable2) { } /** @private */ rebuildRenderFlags() { this.renderFlags.reset(); this._updateRenderFlagsVisibleLayers(); if (this.renderFlags.numLayers > 0 && this.renderFlags.numVisibleLayers === 0) { this.renderFlags.culled = true; return; } this._updateRenderFlags(); } /** * @private */ _updateRenderFlagsVisibleLayers() { const renderFlags = this.renderFlags; renderFlags.numLayers = this.layerList.length; renderFlags.numVisibleLayers = 0; for (let layerIndex = 0, len = this.layerList.length; layerIndex < len; layerIndex++) { const layer = this.layerList[layerIndex]; const layerVisible = this._getActiveSectionPlanesForLayer(layer); if (layerVisible) { renderFlags.visibleLayers[renderFlags.numVisibleLayers++] = layerIndex; } } } /** @private */ _createDummyEntityForUnusedMeshes() { const unusedMeshIds = Object.keys(this._unusedMeshes); if (unusedMeshIds.length > 0) { const entityId = `${this.id}-${math.createUUID()}`; this.warn(`Creating dummy SceneModelEntity "${entityId}" for unused SceneMeshes: [${unusedMeshIds.join(",")}]`); this.createEntity({ id: entityId, meshIds: unusedMeshIds, isObject: true }); } this._unusedMeshes = {}; } _getActiveSectionPlanesForLayer(layer) { const renderFlags = this.renderFlags; const sectionPlanes = this.scene._sectionPlanesState.sectionPlanes; const numSectionPlanes = sectionPlanes.length; const baseIndex = layer.layerIndex * numSectionPlanes; if (numSectionPlanes > 0) { for (let i = 0; i < numSectionPlanes; i++) { const sectionPlane = sectionPlanes[i]; if (!sectionPlane.active) { renderFlags.sectionPlanesActivePerLayer[baseIndex + i] = false; } else { renderFlags.sectionPlanesActivePerLayer[baseIndex + i] = true; renderFlags.sectioned = true; } } } return true; } _updateRenderFlags() { if (this.numVisibleLayerPortions === 0) { return; } if (this.numCulledLayerPortions === this.numPortions) { return; } const renderFlags = this.renderFlags; renderFlags.colorOpaque = (this.numTransparentLayerPortions < this.numPortions); if (this.numTransparentLayerPortions > 0) { renderFlags.colorTransparent = true; } if (this.numXRayedLayerPortions > 0) { const xrayMaterial = this.scene.xrayMaterial._state; if (xrayMaterial.fill) { if (xrayMaterial.fillAlpha < 1.0) { renderFlags.xrayedSilhouetteTransparent = true; } else { renderFlags.xrayedSilhouetteOpaque = true; } } if (xrayMaterial.edges) { if (xrayMaterial.edgeAlpha < 1.0) { renderFlags.xrayedEdgesTransparent = true; } else { renderFlags.xrayedEdgesOpaque = true; } } } if (this.numEdgesLayerPortions > 0) { const edgeMaterial = this.scene.edgeMaterial._state; if (edgeMaterial.edges) { renderFlags.edgesOpaque = (this.numTransparentLayerPortions < this.numPortions); if (this.numTransparentLayerPortions > 0) { renderFlags.edgesTransparent = true; } } } if (this.numSelectedLayerPortions > 0) { const selectedMaterial = this.scene.selectedMaterial._state; if (selectedMaterial.fill) { if (selectedMaterial.fillAlpha < 1.0) { renderFlags.selectedSilhouetteTransparent = true; } else { renderFlags.selectedSilhouetteOpaque = true; } } if (selectedMaterial.edges) { if (selectedMaterial.edgeAlpha < 1.0) { renderFlags.selectedEdgesTransparent = true; } else { renderFlags.selectedEdgesOpaque = true; } } } if (this.numHighlightedLayerPortions > 0) { const highlightMaterial = this.scene.highlightMaterial._state; if (highlightMaterial.fill) { if (highlightMaterial.fillAlpha < 1.0) { renderFlags.highlightedSilhouetteTransparent = true; } else { renderFlags.highlightedSilhouetteOpaque = true; } } if (highlightMaterial.edges) { if (highlightMaterial.edgeAlpha < 1.0) { renderFlags.highlightedEdgesTransparent = true; } else { renderFlags.highlightedEdgesOpaque = true; } } } } // -------------- RENDERING --------------------------------------------------------------------------------------- /** @private */ drawColorOpaque(frameCtx, layerList) { const renderFlags = this.renderFlags; for (let i = 0, len = renderFlags.visibleLayers.length; i < len; i++) { const layerIndex = renderFlags.visibleLayers[i]; this.layerList[layerIndex].drawColorOpaque(renderFlags, frameCtx); } } /** @private */ drawColorTransparent(frameCtx) { const renderFlags = this.renderFlags; for (let i = 0, len = renderFlags.visibleLayers.length; i < len; i++) { const layerIndex = renderFlags.visibleLayers[i]; this.layerList[layerIndex].drawColorTransparent(renderFlags, frameCtx); } } /** @private */ drawDepth(frameCtx) { // Dedicated to SAO because it skips transparent objects const renderFlags = this.renderFlags; for (let i = 0, len = renderFlags.visibleLayers.length; i < len; i++) { const layerIndex = renderFlags.visibleLayers[i]; this.layerList[layerIndex].drawDepth(renderFlags, frameCtx); } } /** @private */ drawNormals(frameCtx) { // Dedicated to SAO because it skips transparent objects const renderFlags = this.renderFlags; for (let i = 0, len = renderFlags.visibleLayers.length; i < len; i++) { const layerIndex = renderFlags.visibleLayers[i]; this.layerList[layerIndex].drawNormals(renderFlags, frameCtx); } } /** @private */ drawSilhouetteXRayed(frameCtx) { const renderFlags = this.renderFlags; for (let i = 0, len = renderFlags.visibleLayers.length; i < len; i++) { const layerIndex = renderFlags.visibleLayers[i]; this.layerList[layerIndex].drawSilhouetteXRayed(renderFlags, frameCtx); } } /** @private */ drawSilhouetteHighlighted(frameCtx) { const renderFlags = this.renderFlags; for (let i = 0, len = renderFlags.visibleLayers.length; i < len; i++) { const layerIndex = renderFlags.visibleLayers[i]; this.layerList[layerIndex].drawSilhouetteHighlighted(renderFlags, frameCtx); } } /** @private */ drawSilhouetteSelected(frameCtx) { const renderFlags = this.renderFlags; for (let i = 0, len = renderFlags.visibleLayers.length; i < len; i++) { const layerIndex = renderFlags.visibleLayers[i]; this.layerList[layerIndex].drawSilhouetteSelected(renderFlags, frameCtx); } } /** @private */ drawEdgesColorOpaque(frameCtx) { const renderFlags = this.renderFlags; for (let i = 0, len = renderFlags.visibleLayers.length; i < len; i++) { const layerIndex = renderFlags.visibleLayers[i]; this.layerList[layerIndex].drawEdgesColorOpaque(renderFlags, frameCtx); } } /** @private */ drawEdgesColorTransparent(frameCtx) { const renderFlags = this.renderFlags; for (let i = 0, len = renderFlags.visibleLayers.length; i < len; i++) { const layerIndex = renderFlags.visibleLayers[i]; this.layerList[layerIndex].drawEdgesColorTransparent(renderFlags, frameCtx); } } /** @private */ drawEdgesXRayed(frameCtx) { const renderFlags = this.renderFlags; for (let i = 0, len = renderFlags.visibleLayers.length; i < len; i++) { const layerIndex = renderFlags.visibleLayers[i]; this.layerList[layerIndex].drawEdgesXRayed(renderFlags, frameCtx); } } /** @private */ drawEdgesHighlighted(frameCtx) { const renderFlags = this.renderFlags; for (let i = 0, len = renderFlags.visibleLayers.length; i < len; i++) { const layerIndex = renderFlags.visibleLayers[i]; this.layerList[layerIndex].drawEdgesHighlighted(renderFlags, frameCtx); } } /** @private */ drawEdgesSelected(frameCtx) { const renderFlags = this.renderFlags; for (let i = 0, len = renderFlags.visibleLayers.length; i < len; i++) { const layerIndex = renderFlags.visibleLayers[i]; this.layerList[layerIndex].drawEdgesSelected(renderFlags, frameCtx); } } /** * @private */ drawOcclusion(frameCtx) { if (this.numVisibleLayerPortions === 0) { return; } const renderFlags = this.renderFlags; for (let i = 0, len = renderFlags.visibleLayers.length; i < len; i++) { const layerIndex = renderFlags.visibleLayers[i]; this.layerList[layerIndex].drawOcclusion(renderFlags, frameCtx); } } /** * @private */ drawShadow(frameCtx) { if (this.numVisibleLayerPortions === 0) { return; } const renderFlags = this.renderFlags; for (let i = 0, len = renderFlags.visibleLayers.length; i < len; i++) { const layerIndex = renderFlags.visibleLayers[i]; this.layerList[layerIndex].drawShadow(renderFlags, frameCtx); } } /** @private */ setPickMatrices(pickViewMatrix, pickProjMatrix) { if (this._numVisibleLayerPortions === 0) { return; } const renderFlags = this.renderFlags; for (let i = 0, len = renderFlags.visibleLayers.length; i < len; i++) { const layerIndex = renderFlags.visibleLayers[i]; const layer = this.layerList[layerIndex]; if (layer.setPickMatrices) { layer.setPickMatrices(pickViewMatrix, pickProjMatrix); } } } /** @private */ drawPickMesh(frameCtx) { if (this.numVisibleLayerPortions === 0) { return; } const renderFlags = this.renderFlags; for (let i = 0, len = renderFlags.visibleLayers.length; i < len; i++) { const layerIndex = renderFlags.visibleLayers[i]; this.layerList[layerIndex].drawPickMesh(renderFlags, frameCtx); } } /** * Called by SceneModelMesh.drawPickDepths() * @private */ drawPickDepths(frameCtx) { if (this.numVisibleLayerPortions === 0) { return; } const renderFlags = this.renderFlags; for (let i = 0, len = renderFlags.visibleLayers.length; i < len; i++) { const layerIndex = renderFlags.visibleLayers[i]; this.layerList[layerIndex].drawPickDepths(renderFlags, frameCtx); } } /** * Called by SceneModelMesh.drawPickNormals() * @private */ drawPickNormals(frameCtx) { if (this.numVisibleLayerPortions === 0) { return; } const renderFlags = this.renderFlags; for (let i = 0, len = renderFlags.visibleLayers.length; i < len; i++) { const layerIndex = renderFlags.visibleLayers[i]; this.layerList[layerIndex].drawPickNormals(renderFlags, frameCtx); } } /** * @private */ drawSnapInit(frameCtx) { if (this.numVisibleLayerPortions === 0) { return; } const renderFlags = this.renderFlags; for (let i = 0, len = renderFlags.visibleLayers.length; i < len; i++) { const layerIndex = renderFlags.visibleLayers[i]; const layer = this.layerList[layerIndex]; if (layer.drawSnapInit) { frameCtx.snapPickOrigin = [0, 0, 0]; frameCtx.snapPickCoordinateScale = [1, 1, 1]; frameCtx.snapPickLayerNumber++; layer.drawSnapInit(renderFlags, frameCtx); frameCtx.snapPickLayerParams[frameCtx.snapPickLayerNumber] = { origin: frameCtx.snapPickOrigin.slice(), coordinateScale: frameCtx.snapPickCoordinateScale.slice(), }; } } } /** * @private */ drawSnap(frameCtx) { if (this.numVisibleLayerPortions === 0) { return; } const renderFlags = this.renderFlags; for (let i = 0, len = renderFlags.visibleLayers.length; i < len; i++) { const layerIndex = renderFlags.visibleLayers[i]; const layer = this.layerList[layerIndex]; if (layer.drawSnap) { frameCtx.snapPickOrigin = [0, 0, 0]; frameCtx.snapPickCoordinateScale = [1, 1, 1]; frameCtx.snapPickLayerNumber++; layer.drawSnap(renderFlags, frameCtx); frameCtx.snapPickLayerParams[frameCtx.snapPickLayerNumber] = { origin: frameCtx.snapPickOrigin.slice(), coordinateScale: frameCtx.snapPickCoordinateScale.slice(), }; } } } /** * Destroys this SceneModel. */ destroy() { for (let layerId in this._vboBatchingLayers) { if (this._vboBatchingLayers.hasOwnProperty(layerId)) { this._vboBatchingLayers[layerId].destroy(); } } this._vboBatchingLayers = {}; for (let layerId in this._vboInstancingLayers) { if (this._vboInstancingLayers.hasOwnProperty(layerId)) { this._vboInstancingLayers[layerId].destroy(); } } this._vboInstancingLayers = {}; this.scene.camera.off(this._onCameraViewMatrix); this.scene.off(this._onTick); for (let i = 0, len = this.layerList.length; i < len; i++) { this.layerList[i].destroy(); } this.layerList = []; for (let i = 0, len = this._entityList.length; i < len; i++) { this._entityList[i]._destroy(); } this._layersToFinalize = {}; // Object.entries(this._geometries).forEach(([id, geometry]) => { // geometry.destroy(); // }); this._geometries = {}; this._dtxBuckets = {}; this._textures = {}; this._textureSets = {}; this._meshes = {}; this._entities = {}; this.scene._aabbDirty = true; if (this._isModel) { this.scene._deregisterModel(this); } putScratchMemory(); super.destroy(); } } /** * This function applies two steps to the provided mesh geometry data: * * - 1st, it reduces its `.positions` to unique positions, thus removing duplicate vertices. It will adjust the `.indices` and `.edgeIndices` array accordingly to the unique `.positions`. * * - 2nd, it tries to do an optimization called `index rebucketting` * * _Rebucketting minimizes the amount of RAM usage for a given mesh geometry by trying do demote its needed index bitness._ * * - _for 32 bit indices, will try to demote them to 16 bit indices_ * - _for 16 bit indices, will try to demote them to 8 bits indices_ * - _8 bits indices are kept as-is_ * * The fact that 32/16/8 bits are needed for indices, depends on the number of maximumm indexable vertices within the mesh geometry: this is, the number of vertices in the mesh geometry. * * The function returns the same provided input `geometry`, enrichened with the additional key `.preparedBukets`. * * @param {object} geometry The mesh information containing `.positions`, `.indices`, `.edgeIndices` arrays. * * @param enableVertexWelding * @param enableIndexBucketing * @returns {object} The mesh information enrichened with `.buckets` key. */ function createDTXBuckets(geometry, enableVertexWelding, enableIndexBucketing) { let uniquePositionsCompressed, uniqueIndices, uniqueEdgeIndices; if (enableVertexWelding || enableIndexBucketing) { // Expensive - careful! [ uniquePositionsCompressed, uniqueIndices, uniqueEdgeIndices, ] = uniquifyPositions({ positionsCompressed: geometry.positionsCompressed, indices: geometry.indices, edgeIndices: geometry.edgeIndices }); } else { uniquePositionsCompressed = geometry.positionsCompressed; uniqueIndices = geometry.indices; uniqueEdgeIndices = geometry.edgeIndices; } let buckets; if (enableIndexBucketing) { let numUniquePositions = uniquePositionsCompressed.length / 3; buckets = rebucketPositions({ positionsCompressed: uniquePositionsCompressed, indices: uniqueIndices, edgeIndices: uniqueEdgeIndices, }, (numUniquePositions > (1 << 16)) ? 16 : 8, // true ); } else { buckets = [{ positionsCompressed: uniquePositionsCompressed, indices: uniqueIndices, edgeIndices: uniqueEdgeIndices, }]; } return buckets; } /** * A set of 3D line segments. * * * Creates a set of 3D line segments. * * Registered by {@link LineSet#id} in {@link Scene#lineSets}. * * Configure color using the {@link LinesMaterial} located at {@link Scene#linesMaterial}. * * {@link BCFViewpointsPlugin} will save and load Linesets in BCF viewpoints. * * ## Usage * * In the example below, we'll load the Schependomlaan model, then use * a ````LineSet```` to show a grid underneath the model. * * [](https://xeokit.github.io/xeokit-sdk/examples/scenegraph/#LineSet_grid) * * [[Run this example](https://xeokit.github.io/xeokit-sdk/examples/scenegraph/#LineSet_grid)] * * ````javascript * import {Viewer, XKTLoaderPlugin, LineSet, buildGridGeometry} from "https://cdn.jsdelivr.net/npm/@xeokit/xeokit-sdk/dist/xeokit-sdk.es.min.js"; * * const viewer = new Viewer({ * canvasId: "myCanvas", * transparent: true * }); * * const camera = viewer.camera; * * viewer.camera.eye = [-2.56, 8.38, 8.27]; * viewer.camera.look = [13.44, 3.31, -14.83]; * viewer.camera.up = [0.10, 0.98, -0.14]; * * const xktLoader = new XKTLoaderPlugin(viewer); * * const model = xktLoader.load({ * id: "myModel", * src: "../assets/models/xkt/v8/ifc/Schependomlaan.ifc.xkt", * position: [0,1,0], * edges: true, * saoEnabled: true * }); * * const geometryArrays = buildGridGeometry({ * size: 100, * divisions: 30 * }); * * new LineSet(viewer.scene, { * positions: geometryArrays.positions, * indices: geometryArrays.indices * }); * ```` */ class LineSet extends Component { /** * Creates a new LineSet. * * Registers the LineSet in {@link Scene#lineSets}; causes Scene to fire a "lineSetCreated" event. * * @constructor * @param {Component} [owner] Owner component. When destroyed, the owner will destroy this ````LineSet```` as well. * @param {*} [cfg] ````LineSet```` configuration * @param {String} [cfg.id] Optional ID, unique among all components in the parent {@link Scene}, generated automatically when omitted. * @param {Number[]} cfg.positions World-space 3D vertex positions. * @param {Number[]} [cfg.indices] Indices to connect ````positions```` into line segments. Note that these are separate line segments, not a polyline. * @param {Number[]} [cfg.color=[1,1,1]] The color of this ````LineSet````. This is both emissive and diffuse. * @param {Boolean} [cfg.visible=true] Indicates whether or not this ````LineSet```` is visible. * @param {Number} [cfg.opacity=1.0] ````LineSet````'s initial opacity factor. */ constructor(owner, cfg = {}) { super(owner, cfg); this._positions = cfg.positions || []; if (cfg.indices) { this._indices = cfg.indices; } else { this._indices = []; for (let i = 0, len = (this._positions.length / 3) - 1; i < len; i += 2) { this._indices.push(i); this._indices.push(i + 1); } } this._color = cfg.color || [1, 1, 1]; this._opacity = cfg.opacity || 1.0; this._sceneModel = new SceneModel(this, { isModel: false // Don't register in Scene.models }); this._sceneModel.createMesh({ id: "linesMesh", primitive: "lines", positions: this._positions, indices: this._indices, color: this._color, opacity: this._opacity, }); this._sceneModel.createEntity({ meshIds: ["linesMesh"], visible: cfg.visible, clippable: cfg.clippable, collidable: cfg.collidable }); this._sceneModel.finalize(); this.scene._lineSetCreated(this); } /** * Sets if this ````LineSet```` is visible. * * Default value is ````true````. * * @param {Boolean} visible Set ````true```` to make this ````LineSet```` visible. */ set visible(visible) { this._sceneModel.visible = visible; } /** * Gets if this ````LineSet```` is visible. * * Default value is ````true````. * * @returns {Boolean} Returns ````true```` if visible. */ get visible() { return this._sceneModel.visible; } /** * Gets the 3D World-space vertex positions of the lines in this ````LineSet````. * * @returns {Number[]} */ get positions() { return this._positions; } /** * Gets the vertex indices of the lines in this ````LineSet````. * * @returns {Number[]} */ get indices() { return this._indices; } /** * Destroys this ````LineSet````. * * Removes the ```LineSet```` from {@link Scene#lineSets}; causes Scene to fire a "lineSetDestroyed" event. */ destroy() { super.destroy(); // destroyes _sceneModel this.scene._lineSetDestroyed(this); } } /** * @desc A dynamic light source within a {@link Scene}. * * These are registered by {@link Light#id} in {@link Scene#lights}. */ class Light extends Component { /** @private */ get type() { return "Light"; } /** * @private */ get isLight() { return true; } constructor(owner, cfg = {}) { super(owner, cfg); } } /** * @desc An ambient light source of fixed color and intensity that illuminates all {@link Mesh}es equally. * * * {@link AmbientLight#color} multiplies by {@link PhongMaterial#ambient} at each position of each {@link ReadableGeometry} surface. * * {@link AmbientLight#color} multiplies by {@link LambertMaterial#color} uniformly across each triangle of each {@link ReadableGeometry} (ie. flat shaded). * * {@link AmbientLight}s, {@link DirLight}s and {@link PointLight}s are registered by their {@link Component#id} on {@link Scene#lights}. * * ## Usage * * In the example below we'll destroy the {@link Scene}'s default light sources then create an AmbientLight and a couple of {@link @DirLight}s: * * [[Run this example](/examples/index.html#lights_AmbientLight)] * * ````javascript * import {Viewer, Mesh, buildTorusGeometry, * ReadableGeometry, PhongMaterial, Texture, AmbientLight} from "xeokit-sdk.es.js"; * * // Create a Viewer and arrange the camera * * const viewer = new Viewer({ * canvasId: "myCanvas" * }); * * viewer.scene.camera.eye = [0, 0, 5]; * viewer.scene.camera.look = [0, 0, 0]; * viewer.scene.camera.up = [0, 1, 0]; * * // Replace the Scene's default lights with a single custom AmbientLight * * viewer.scene.clearLights(); * * new AmbientLight(viewer.scene, { * color: [0.0, 0.3, 0.7], * intensity: 1.0 * }); * * new DirLight(viewer.scene, { * id: "keyLight", * dir: [0.8, -0.6, -0.8], * color: [1.0, 0.3, 0.3], * intensity: 1.0, * space: "view" * }); * * new DirLight(viewer.scene, { * id: "fillLight", * dir: [-0.8, -0.4, -0.4], * color: [0.3, 1.0, 0.3], * intensity: 1.0, * space: "view" * }); * * new DirLight(viewer.scene, { * id: "rimLight", * dir: [0.2, -0.8, 0.8], * color: [0.6, 0.6, 0.6], * intensity: 1.0, * space: "view" * }); * * // Create a mesh with torus shape and PhongMaterial * * new Mesh(viewer.scene, { * geometry: new ReadableGeometry(viewer.scene, buildSphereGeometry({ * center: [0, 0, 0], * radius: 1.5, * tube: 0.5, * radialSegments: 32, * tubeSegments: 24, * arc: Math.PI * 2.0 * }), * material: new PhongMaterial(viewer.scene, { * ambient: [1.0, 1.0, 1.0], * shininess: 30, * diffuseMap: new Texture(viewer.scene, { * src: "textures/diffuse/uvGrid2.jpg" * }) * }) * }); * * // Adjust the color of our AmbientLight * * var ambientLight = viewer.scene.lights["myAmbientLight"]; * ambientLight.color = [1.0, 0.8, 0.8]; *```` */ class AmbientLight extends Light { /** @private */ get type() { return "AmbientLight"; } /** * @param {Component} owner Owner component. When destroyed, the owner will destroy this AmbientLight as well. * @param {*} [cfg] AmbientLight configuration * @param {String} [cfg.id] Optional ID, unique among all components in the parent {@link Scene}, generated automatically when omitted. * @param {Number[]} [cfg.color=[0.7, 0.7, 0.8]] The color of this AmbientLight. * @param {Number} [cfg.intensity=[1.0]] The intensity of this AmbientLight, as a factor in range ````[0..1]````. */ constructor(owner, cfg = {}) { super(owner, cfg); this._state = { type: "ambient", color: math.vec3([0.7, 0.7, 0.7]), intensity: 1.0 }; this.color = cfg.color; this.intensity = cfg.intensity; this.scene._lightCreated(this); } /** * Sets the RGB color of this AmbientLight. * * Default value is ````[0.7, 0.7, 0.8]````. * * @param {Number[]} color The AmbientLight's RGB color. */ set color(color) { this._state.color.set(color || [0.7, 0.7, 0.8]); this.glRedraw(); } /** * Gets the RGB color of this AmbientLight. * * Default value is ````[0.7, 0.7, 0.8]````. * * @returns {Number[]} The AmbientLight's RGB color. */ get color() { return this._state.color; } /** * Sets the intensity of this AmbientLight. * * Default value is ````1.0```` for maximum intensity. * * @param {Number} intensity The AmbientLight's intensity. */ set intensity(intensity) { this._state.intensity = intensity !== undefined ? intensity : 1.0; this.glRedraw(); } /** * Gets the intensity of this AmbientLight. * * Default value is ````1.0```` for maximum intensity. * * @returns {Number} The AmbientLight's intensity. */ get intensity() { return this._state.intensity; } /** * Destroys this AmbientLight. */ destroy() { super.destroy(); this.scene._lightDestroyed(this); } } /** * @desc Represents a WebGL render buffer. * @private */ class RenderBuffer { constructor(canvas, gl, options) { options = options || {}; /** @type {WebGL2RenderingContext} */ this.gl = gl; this.allocated = false; this.canvas = canvas; this.buffer = null; this.bound = false; this.size = options.size; this._hasDepthTexture = !!options.depthTexture; } setSize(size) { this.size = size; } webglContextRestored(gl) { this.gl = gl; this.buffer = null; this.allocated = false; this.bound = false; } bind(...internalformats) { this._touch(...internalformats); if (this.bound) { return; } const gl = this.gl; gl.bindFramebuffer(gl.FRAMEBUFFER, this.buffer.framebuf); this.bound = true; } /** * Create and specify a WebGL texture image. * * @param { number } width * @param { number } height * @param { GLenum } [internalformat=null] * * @returns { WebGLTexture } */ createTexture(width, height, internalformat = null) { const gl = this.gl; const colorTexture = gl.createTexture(); gl.bindTexture(gl.TEXTURE_2D, colorTexture); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); if (internalformat) { gl.texStorage2D(gl.TEXTURE_2D, 1, internalformat, width, height); } else { gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, null); } return colorTexture; } /** * * @param {number[]} [internalformats=[]] * @returns */ _touch(...internalformats) { let width; let height; const gl = this.gl; if (this.size) { width = this.size[0]; height = this.size[1]; } else { width = gl.drawingBufferWidth; height = gl.drawingBufferHeight; } if (this.buffer) { if (this.buffer.width === width && this.buffer.height === height) { return; } else { this.buffer.textures.forEach(texture => gl.deleteTexture(texture)); gl.deleteFramebuffer(this.buffer.framebuf); gl.deleteRenderbuffer(this.buffer.renderbuf); } } const colorTextures = []; if (internalformats.length > 0) { colorTextures.push(...internalformats.map(internalformat => this.createTexture(width, height, internalformat))); } else { colorTextures.push(this.createTexture(width, height)); } let depthTexture; if (this._hasDepthTexture) { depthTexture = gl.createTexture(); gl.bindTexture(gl.TEXTURE_2D, depthTexture); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); gl.texImage2D(gl.TEXTURE_2D, 0, gl.DEPTH_COMPONENT32F, width, height, 0, gl.DEPTH_COMPONENT, gl.FLOAT, null); } const renderbuf = gl.createRenderbuffer(); gl.bindRenderbuffer(gl.RENDERBUFFER, renderbuf); gl.renderbufferStorage(gl.RENDERBUFFER, gl.DEPTH_COMPONENT32F, width, height); const framebuf = gl.createFramebuffer(); gl.bindFramebuffer(gl.FRAMEBUFFER, framebuf); for (let i = 0; i < colorTextures.length; i++) { gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0 + i, gl.TEXTURE_2D, colorTextures[i], 0); } if (internalformats.length > 0) { gl.drawBuffers(colorTextures.map((_, i) => gl.COLOR_ATTACHMENT0 + i)); } if (this._hasDepthTexture) { gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.TEXTURE_2D, depthTexture, 0); } else { gl.framebufferRenderbuffer(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.RENDERBUFFER, renderbuf); } gl.bindTexture(gl.TEXTURE_2D, null); gl.bindRenderbuffer(gl.RENDERBUFFER, null); gl.bindFramebuffer(gl.FRAMEBUFFER, null); // Verify framebuffer is OK gl.bindFramebuffer(gl.FRAMEBUFFER, framebuf); if (!gl.isFramebuffer(framebuf)) { throw "Invalid framebuffer"; } gl.bindFramebuffer(gl.FRAMEBUFFER, null); const status = gl.checkFramebufferStatus(gl.FRAMEBUFFER); switch (status) { case gl.FRAMEBUFFER_COMPLETE: break; case gl.FRAMEBUFFER_INCOMPLETE_ATTACHMENT: throw "Incomplete framebuffer: FRAMEBUFFER_INCOMPLETE_ATTACHMENT"; case gl.FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT: throw "Incomplete framebuffer: FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT"; case gl.FRAMEBUFFER_INCOMPLETE_DIMENSIONS: throw "Incomplete framebuffer: FRAMEBUFFER_INCOMPLETE_DIMENSIONS"; case gl.FRAMEBUFFER_UNSUPPORTED: throw "Incomplete framebuffer: FRAMEBUFFER_UNSUPPORTED"; default: throw "Incomplete framebuffer: " + status; } this.buffer = { framebuf: framebuf, renderbuf: renderbuf, texture: colorTextures[0], textures: colorTextures, depthTexture: depthTexture, width: width, height: height }; this.bound = false; } clear() { if (!this.bound) { throw "Render buffer not bound"; } const gl = this.gl; gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); } read(pickX, pickY, glFormat = null, glType = null, arrayType = Uint8Array, arrayMultiplier = 4, colorBufferIndex = 0) { const x = pickX; const y = this.buffer.height ? (this.buffer.height - pickY - 1) : (this.gl.drawingBufferHeight - pickY); const pix = new arrayType(arrayMultiplier); const gl = this.gl; gl.readBuffer(gl.COLOR_ATTACHMENT0 + colorBufferIndex); gl.readPixels(x, y, 1, 1, glFormat || gl.RGBA, glType || gl.UNSIGNED_BYTE, pix, 0); return pix; } readArray(glFormat = null, glType = null, arrayType = Uint8Array, arrayMultiplier = 4, colorBufferIndex = 0) { const pix = new arrayType(this.buffer.width*this.buffer.height * arrayMultiplier); const gl = this.gl; gl.readBuffer(gl.COLOR_ATTACHMENT0 + colorBufferIndex); gl.readPixels(0, 0, this.buffer.width, this.buffer.height, glFormat || gl.RGBA, glType || gl.UNSIGNED_BYTE, pix, 0); return pix; } /** * Returns an HTMLCanvas containing the contents of the RenderBuffer as an image. * * - The HTMLCanvas has a CanvasRenderingContext2D. * - Expects the caller to draw more things on the HTMLCanvas (annotations etc). * * @returns {HTMLCanvasElement} */ readImageAsCanvas() { const gl = this.gl; const imageDataCache = this._getImageDataCache(); const pixelData = imageDataCache.pixelData; const canvas = imageDataCache.canvas; const imageData = imageDataCache.imageData; const context = imageDataCache.context; gl.readPixels(0, 0, this.buffer.width, this.buffer.height, gl.RGBA, gl.UNSIGNED_BYTE, pixelData); const width = this.buffer.width; const height = this.buffer.height; const halfHeight = height / 2 | 0; // the | 0 keeps the result an int const bytesPerRow = width * 4; const temp = new Uint8Array(width * 4); for (let y = 0; y < halfHeight; ++y) { const topOffset = y * bytesPerRow; const bottomOffset = (height - y - 1) * bytesPerRow; temp.set(pixelData.subarray(topOffset, topOffset + bytesPerRow)); pixelData.copyWithin(topOffset, bottomOffset, bottomOffset + bytesPerRow); pixelData.set(temp, bottomOffset); } imageData.data.set(pixelData); context.putImageData(imageData, 0, 0); return canvas; } readImage(params) { const gl = this.gl; const imageDataCache = this._getImageDataCache(); const pixelData = imageDataCache.pixelData; const canvas = imageDataCache.canvas; const imageData = imageDataCache.imageData; const context = imageDataCache.context; const { width, height } = this.buffer; gl.readPixels(0, 0, width, height, gl.RGBA, gl.UNSIGNED_BYTE, pixelData); imageData.data.set(pixelData); context.putImageData(imageData, 0, 0); // flip Y context.save(); context.globalCompositeOperation = 'copy'; context.scale(1, -1); context.drawImage(canvas, 0, -height, width, height); context.restore(); let format = params.format || "png"; if (format !== "jpeg" && format !== "png" && format !== "bmp") { console.error("Unsupported image format: '" + format + "' - supported types are 'jpeg', 'bmp' and 'png' - defaulting to 'png'"); format = "png"; } return canvas.toDataURL(`image/${format}`); } _getImageDataCache(type = Uint8Array, multiplier = 4) { const bufferWidth = this.buffer.width; const bufferHeight = this.buffer.height; let imageDataCache = this._imageDataCache; if (imageDataCache) { if (imageDataCache.width !== bufferWidth || imageDataCache.height !== bufferHeight) { this._imageDataCache = null; imageDataCache = null; } } if (!imageDataCache) { const canvas = document.createElement('canvas'); const context = canvas.getContext('2d'); canvas.width = bufferWidth; canvas.height = bufferHeight; imageDataCache = { pixelData: new type(bufferWidth * bufferHeight * multiplier), canvas: canvas, context: context, imageData: context.createImageData(bufferWidth, bufferHeight), width: bufferWidth, height: bufferHeight }; this._imageDataCache = imageDataCache; } imageDataCache.context.resetTransform(); // Prevents strange scale-accumulation effect with html2canvas return imageDataCache; } unbind() { const gl = this.gl; gl.bindFramebuffer(gl.FRAMEBUFFER, null); this.bound = false; } getTexture(index = 0) { const self = this; return this._texture || (this._texture = { renderBuffer: this, bind: function (unit) { if (self.buffer && self.buffer.textures[index]) { self.gl.activeTexture(self.gl["TEXTURE" + unit]); self.gl.bindTexture(self.gl.TEXTURE_2D, self.buffer.textures[index]); return true; } return false; }, unbind: function (unit) { if (self.buffer && self.buffer.textures[index]) { self.gl.activeTexture(self.gl["TEXTURE" + unit]); self.gl.bindTexture(self.gl.TEXTURE_2D, null); } } }); } hasDepthTexture() { return this._hasDepthTexture; } getDepthTexture() { if (!this._hasDepthTexture) { return null; } const self = this; return this._depthTexture || (this._dethTexture = { renderBuffer: this, bind: function (unit) { if (self.buffer && self.buffer.depthTexture) { self.gl.activeTexture(self.gl["TEXTURE" + unit]); self.gl.bindTexture(self.gl.TEXTURE_2D, self.buffer.depthTexture); return true; } return false; }, unbind: function (unit) { if (self.buffer && self.buffer.depthTexture) { self.gl.activeTexture(self.gl["TEXTURE" + unit]); self.gl.bindTexture(self.gl.TEXTURE_2D, null); } } }); } destroy() { if (this.allocated) { const gl = this.gl; this.buffer.textures.forEach(texture => gl.deleteTexture(texture)); gl.deleteTexture(this.buffer.depthTexture); gl.deleteFramebuffer(this.buffer.framebuf); gl.deleteRenderbuffer(this.buffer.renderbuf); this.allocated = false; this.buffer = null; this.bound = false; } this._imageDataCache = null; this._texture = null; this._depthTexture = null; } } /** * @desc A directional light source that illuminates all {@link Mesh}es equally from a given direction. * * * Has an emission direction vector in {@link DirLight#dir}, but no position. * * Defined in either *World* or *View* coordinate space. When in World-space, {@link DirLight#dir} is relative to the * World coordinate system, and will appear to move as the {@link Camera} moves. When in View-space, {@link DirLight#dir} is * relative to the View coordinate system, and will behave as if fixed to the viewer's head. * * {@link AmbientLight}s, {@link DirLight}s and {@link PointLight}s are registered by their {@link Component#id} on {@link Scene#lights}. * * ## Usage * * In the example below we'll replace the {@link Scene}'s default light sources with three View-space DirLights. * * [[Run this example](/examples/index.html#lights_DirLight_view)] * * ````javascript * import {Viewer, Mesh, buildSphereGeometry, * buildPlaneGeometry, ReadableGeometry, * PhongMaterial, Texture, DirLight} from "xeokit-sdk.es.js"; * * // Create a Viewer and arrange the camera * * const viewer = new Viewer({ * canvasId: "myCanvas" * }); * * viewer.scene.camera.eye = [0, 0, 5]; * viewer.scene.camera.look = [0, 0, 0]; * viewer.scene.camera.up = [0, 1, 0]; * * // Replace the Scene's default lights with three custom view-space DirLights * * viewer.scene.clearLights(); * * new DirLight(viewer.scene, { * id: "keyLight", * dir: [0.8, -0.6, -0.8], * color: [1.0, 0.3, 0.3], * intensity: 1.0, * space: "view" * }); * * new DirLight(viewer.scene, { * id: "fillLight", * dir: [-0.8, -0.4, -0.4], * color: [0.3, 1.0, 0.3], * intensity: 1.0, * space: "view" * }); * * new DirLight(viewer.scene, { * id: "rimLight", * dir: [0.2, -0.8, 0.8], * color: [0.6, 0.6, 0.6], * intensity: 1.0, * space: "view" * }); * * * // Create a sphere and ground plane * * new Mesh(viewer.scene, { * geometry: new ReadableGeometry(viewer.scene, buildSphereGeometry({ * radius: 2.0 * }), * material: new PhongMaterial(viewer.scene, { * diffuse: [0.7, 0.7, 0.7], * specular: [1.0, 1.0, 1.0], * emissive: [0, 0, 0], * alpha: 1.0, * ambient: [1, 1, 0], * diffuseMap: new Texture(viewer.scene, { * src: "textures/diffuse/uvGrid2.jpg" * }) * }) * }); * * new Mesh(viewer.scene, { * geometry: buildPlaneGeometry(ReadableGeometry, viewer.scene, { * xSize: 30, * zSize: 30 * }), * material: new PhongMaterial(viewer.scene, { * diffuseMap: new Texture(viewer.scene, { * src: "textures/diffuse/uvGrid2.jpg" * }), * backfaces: true * }), * position: [0, -2.1, 0] * }); * ```` */ class DirLight extends Light { /** @private */ get type() { return "DirLight"; } /** * @param {Component} owner Owner component. When destroyed, the owner will destroy this DirLight as well. * @param {*} [cfg] The DirLight configuration * @param {String} [cfg.id] Optional ID, unique among all components in the parent {@link Scene}, generated automatically when omitted. * @param {Number[]} [cfg.dir=[1.0, 1.0, 1.0]] A unit vector indicating the direction that the light is shining, given in either World or View space, depending on the value of the ````space```` parameter. * @param {Number[]} [cfg.color=[0.7, 0.7, 0.8 ]] The color of this DirLight. * @param {Number} [cfg.intensity=1.0] The intensity of this DirLight, as a factor in range ````[0..1]````. * @param {String} [cfg.space="view"] The coordinate system the DirLight is defined in - ````"view"```` or ````"space"````. * @param {Boolean} [cfg.castsShadow=false] Flag which indicates if this DirLight casts a castsShadow. */ constructor(owner, cfg = {}) { super(owner, cfg); this._shadowRenderBuf = null; this._shadowViewMatrix = null; this._shadowProjMatrix = null; this._shadowViewMatrixDirty = true; this._shadowProjMatrixDirty = true; const camera = this.scene.camera; const canvas = this.scene.canvas; this._onCameraViewMatrix = camera.on("viewMatrix", () => { this._shadowViewMatrixDirty = true; }); this._onCameraProjMatrix = camera.on("projMatrix", () => { this._shadowProjMatrixDirty = true; }); this._onCanvasBoundary = canvas.on("boundary", () => { this._shadowProjMatrixDirty = true; }); this._state = new RenderState({ type: "dir", dir: math.vec3([1.0, 1.0, 1.0]), color: math.vec3([0.7, 0.7, 0.8]), intensity: 1.0, space: cfg.space || "view", castsShadow: false, getShadowViewMatrix: () => { if (this._shadowViewMatrixDirty) { if (!this._shadowViewMatrix) { this._shadowViewMatrix = math.identityMat4(); } const camera = this.scene.camera; const dir = this._state.dir; const look = camera.look; const eye = [look[0] - dir[0], look[1] - dir[1], look[2] - dir[2]]; const up = [0, 1, 0]; math.lookAtMat4v(eye, look, up, this._shadowViewMatrix); this._shadowViewMatrixDirty = false; } return this._shadowViewMatrix; }, getShadowProjMatrix: () => { if (this._shadowProjMatrixDirty) { // TODO: Set when canvas resizes if (!this._shadowProjMatrix) { this._shadowProjMatrix = math.identityMat4(); } math.orthoMat4c(-40, 40, -40, 40, -40.0, 80, this._shadowProjMatrix); // left, right, bottom, top, near, far, dest this._shadowProjMatrixDirty = false; } return this._shadowProjMatrix; }, getShadowRenderBuf: () => { if (!this._shadowRenderBuf) { this._shadowRenderBuf = new RenderBuffer(this.scene.canvas.canvas, this.scene.canvas.gl, {size: [1024, 1024]}); // Super old mobile devices have a limit of 1024x1024 textures } return this._shadowRenderBuf; } }); this.dir = cfg.dir; this.color = cfg.color; this.intensity = cfg.intensity; this.castsShadow = cfg.castsShadow; this.scene._lightCreated(this); } /** * Sets the direction in which the DirLight is shining. * * Default value is ````[1.0, 1.0, 1.0]````. * * @param {Number[]} value The direction vector. */ set dir(value) { this._state.dir.set(value || [1.0, 1.0, 1.0]); this._shadowViewMatrixDirty = true; this.glRedraw(); } /** * Gets the direction in which the DirLight is shining. * * Default value is ````[1.0, 1.0, 1.0]````. * * @returns {Number[]} The direction vector. */ get dir() { return this._state.dir; } /** * Sets the RGB color of this DirLight. * * Default value is ````[0.7, 0.7, 0.8]````. * * @param {Number[]} color The DirLight's RGB color. */ set color(color) { this._state.color.set(color || [0.7, 0.7, 0.8]); this.glRedraw(); } /** * Gets the RGB color of this DirLight. * * Default value is ````[0.7, 0.7, 0.8]````. * * @returns {Number[]} The DirLight's RGB color. */ get color() { return this._state.color; } /** * Sets the intensity of this DirLight. * * Default intensity is ````1.0```` for maximum intensity. * * @param {Number} intensity The DirLight's intensity */ set intensity(intensity) { intensity = intensity !== undefined ? intensity : 1.0; this._state.intensity = intensity; this.glRedraw(); } /** * Gets the intensity of this DirLight. * * Default value is ````1.0```` for maximum intensity. * * @returns {Number} The DirLight's intensity. */ get intensity() { return this._state.intensity; } /** * Sets if this DirLight casts a shadow. * * Default value is ````false````. * * @param {Boolean} castsShadow Set ````true```` to cast shadows. */ set castsShadow(castsShadow) { castsShadow = !!castsShadow; if (this._state.castsShadow === castsShadow) { return; } this._state.castsShadow = castsShadow; this._shadowViewMatrixDirty = true; this.glRedraw(); } /** * Gets if this DirLight casts a shadow. * * Default value is ````false````. * * @returns {Boolean} ````true```` if this DirLight casts shadows. */ get castsShadow() { return this._state.castsShadow; } /** * Destroys this DirLight. */ destroy() { const camera = this.scene.camera; const canvas = this.scene.canvas; camera.off(this._onCameraViewMatrix); camera.off(this._onCameraProjMatrix); canvas.off(this._onCanvasBoundary); super.destroy(); this._state.destroy(); if (this._shadowRenderBuf) { this._shadowRenderBuf.destroy(); } this.scene._lightDestroyed(this); this.glRedraw(); } } /** * A positional light source that originates from a single point and spreads outward in all directions, with optional attenuation over distance. * * * Has a position in {@link PointLight#pos}, but no direction. * * Defined in either *World* or *View* coordinate space. When in World-space, {@link PointLight#pos} is relative to * the World coordinate system, and will appear to move as the {@link Camera} moves. When in View-space, * {@link PointLight#pos} is relative to the View coordinate system, and will behave as if fixed to the viewer's head. * * Has {@link PointLight#constantAttenuation}, {@link PointLight#linearAttenuation} and {@link PointLight#quadraticAttenuation} * factors, which indicate how intensity attenuates over distance. * * {@link AmbientLight}s, {@link PointLight}s and {@link PointLight}s are registered by their {@link Component#id} on {@link Scene#lights}. * * ## Usage * * In the example below we'll replace the {@link Scene}'s default light sources with three World-space PointLights. * * [[Run this example](/examples/index.html#lights_PointLight_world)] * * ````javascript * import {Viewer, Mesh, buildSphereGeometry, buildPlaneGeometry, * ReadableGeometry, PhongMaterial, Texture, PointLight} from "xeokit-sdk.es.js"; * * // Create a Viewer and arrange the camera * * const viewer = new Viewer({ * canvasId: "myCanvas" * }); * * viewer.scene.camera.eye = [0, 0, 5]; * viewer.scene.camera.look = [0, 0, 0]; * viewer.scene.camera.up = [0, 1, 0]; * * // Replace the Scene's default lights with three custom world-space PointLights * * viewer.scene.clearLights(); * * new PointLight(viewer.scene,{ * id: "keyLight", * pos: [-80, 60, 80], * color: [1.0, 0.3, 0.3], * intensity: 1.0, * space: "world" * }); * * new PointLight(viewer.scene,{ * id: "fillLight", * pos: [80, 40, 40], * color: [0.3, 1.0, 0.3], * intensity: 1.0, * space: "world" * }); * * new PointLight(viewer.scene,{ * id: "rimLight", * pos: [-20, 80, -80], * color: [0.6, 0.6, 0.6], * intensity: 1.0, * space: "world" * }); * * // Create a sphere and ground plane * * new Mesh(viewer.scene, { * geometry: new ReadableGeometry(viewer.scene, buildSphereGeometry({ * radius: 1.3 * }), * material: new PhongMaterial(viewer.scene, { * diffuse: [0.7, 0.7, 0.7], * specular: [1.0, 1.0, 1.0], * emissive: [0, 0, 0], * alpha: 1.0, * ambient: [1, 1, 0], * diffuseMap: new Texture(viewer.scene, { * src: "textures/diffuse/uvGrid2.jpg" * }) * }) * }); * * new Mesh(viewer.scene, { * geometry: buildPlaneGeometry(ReadableGeometry, viewer.scene, { * xSize: 30, * zSize: 30 * }), * material: new PhongMaterial(viewer.scene, { * diffuseMap: new Texture(viewer.scene, { * src: "textures/diffuse/uvGrid2.jpg" * }), * backfaces: true * }), * position: [0, -2.1, 0] * }); * ```` */ class PointLight extends Light { /** @private */ get type() { return "PointLight"; } /** * @param {Component} owner Owner component. When destroyed, the owner will destroy this PointLight as well. * @param {*} [cfg] The PointLight configuration * @param {String} [cfg.id] Optional ID, unique among all components in the parent {@link Scene}, generated automatically when omitted. * @param {Number[]} [cfg.pos=[ 1.0, 1.0, 1.0 ]] Position, in either World or View space, depending on the value of the **space** parameter. * @param {Number[]} [cfg.color=[0.7, 0.7, 0.8 ]] Color of this PointLight. * @param {Number} [cfg.intensity=1.0] Intensity of this PointLight, as a factor in range ````[0..1]````. * @param {Number} [cfg.constantAttenuation=0] Constant attenuation factor. * @param {Number} [cfg.linearAttenuation=0] Linear attenuation factor. * @param {Number} [cfg.quadraticAttenuation=0]Quadratic attenuation factor. * @param {String} [cfg.space="view"]The coordinate system this PointLight is defined in - "view" or "world". * @param {Boolean} [cfg.castsShadow=false] Flag which indicates if this PointLight casts a castsShadow. */ constructor(owner, cfg = {}) { super(owner, cfg); const self = this; this._shadowRenderBuf = null; this._shadowViewMatrix = null; this._shadowProjMatrix = null; this._shadowViewMatrixDirty = true; this._shadowProjMatrixDirty = true; const camera = this.scene.camera; const canvas = this.scene.canvas; this._onCameraViewMatrix = camera.on("viewMatrix", () => { this._shadowViewMatrixDirty = true; }); this._onCameraProjMatrix = camera.on("projMatrix", () => { this._shadowProjMatrixDirty = true; }); this._onCanvasBoundary = canvas.on("boundary", () => { this._shadowProjMatrixDirty = true; }); this._state = new RenderState({ type: "point", pos: math.vec3([1.0, 1.0, 1.0]), color: math.vec3([0.7, 0.7, 0.8]), intensity: 1.0, attenuation: [0.0, 0.0, 0.0], space: cfg.space || "view", castsShadow: false, getShadowViewMatrix: () => { if (self._shadowViewMatrixDirty) { if (!self._shadowViewMatrix) { self._shadowViewMatrix = math.identityMat4(); } const eye = self._state.pos; const look = camera.look; const up = camera.up; math.lookAtMat4v(eye, look, up, self._shadowViewMatrix); self._shadowViewMatrixDirty = false; } return self._shadowViewMatrix; }, getShadowProjMatrix: () => { if (self._shadowProjMatrixDirty) { // TODO: Set when canvas resizes if (!self._shadowProjMatrix) { self._shadowProjMatrix = math.identityMat4(); } const canvas = self.scene.canvas.canvas; math.perspectiveMat4(70 * (Math.PI / 180.0), canvas.clientWidth / canvas.clientHeight, 0.1, 500.0, self._shadowProjMatrix); self._shadowProjMatrixDirty = false; } return self._shadowProjMatrix; }, getShadowRenderBuf: () => { if (!self._shadowRenderBuf) { self._shadowRenderBuf = new RenderBuffer(self.scene.canvas.canvas, self.scene.canvas.gl, {size: [1024, 1024]}); // Super old mobile devices have a limit of 1024x1024 textures } return self._shadowRenderBuf; } }); this.pos = cfg.pos; this.color = cfg.color; this.intensity = cfg.intensity; this.constantAttenuation = cfg.constantAttenuation; this.linearAttenuation = cfg.linearAttenuation; this.quadraticAttenuation = cfg.quadraticAttenuation; this.castsShadow = cfg.castsShadow; this.scene._lightCreated(this); } /** * Sets the position of this PointLight. * * This will be either World- or View-space, depending on the value of {@link PointLight#space}. * * Default value is ````[1.0, 1.0, 1.0]````. * * @param {Number[]} pos The position. */ set pos(pos) { this._state.pos.set(pos || [1.0, 1.0, 1.0]); this._shadowViewMatrixDirty = true; this.glRedraw(); } /** * Gets the position of this PointLight. * * This will be either World- or View-space, depending on the value of {@link PointLight#space}. * * Default value is ````[1.0, 1.0, 1.0]````. * * @returns {Number[]} The position. */ get pos() { return this._state.pos; } /** * Sets the RGB color of this PointLight. * * Default value is ````[0.7, 0.7, 0.8]````. * * @param {Number[]} color The PointLight's RGB color. */ set color(color) { this._state.color.set(color || [0.7, 0.7, 0.8]); this.glRedraw(); } /** * Gets the RGB color of this PointLight. * * Default value is ````[0.7, 0.7, 0.8]````. * * @returns {Number[]} The PointLight's RGB color. */ get color() { return this._state.color; } /** * Sets the intensity of this PointLight. * * Default intensity is ````1.0```` for maximum intensity. * * @param {Number} intensity The PointLight's intensity */ set intensity(intensity) { intensity = intensity !== undefined ? intensity : 1.0; this._state.intensity = intensity; this.glRedraw(); } /** * Gets the intensity of this PointLight. * * Default value is ````1.0```` for maximum intensity. * * @returns {Number} The PointLight's intensity. */ get intensity() { return this._state.intensity; } /** * Sets the constant attenuation factor for this PointLight. * * Default value is ````0````. * * @param {Number} value The constant attenuation factor. */ set constantAttenuation(value) { this._state.attenuation[0] = value || 0.0; this.glRedraw(); } /** * Gets the constant attenuation factor for this PointLight. * * Default value is ````0````. * * @returns {Number} The constant attenuation factor. */ get constantAttenuation() { return this._state.attenuation[0]; } /** * Sets the linear attenuation factor for this PointLight. * * Default value is ````0````. * * @param {Number} value The linear attenuation factor. */ set linearAttenuation(value) { this._state.attenuation[1] = value || 0.0; this.glRedraw(); } /** * Gets the linear attenuation factor for this PointLight. * * Default value is ````0````. * * @returns {Number} The linear attenuation factor. */ get linearAttenuation() { return this._state.attenuation[1]; } /** * Sets the quadratic attenuation factor for this PointLight. * * Default value is ````0````. * * @param {Number} value The quadratic attenuation factor. */ set quadraticAttenuation(value) { this._state.attenuation[2] = value || 0.0; this.glRedraw(); } /** * Gets the quadratic attenuation factor for this PointLight. * * Default value is ````0````. * * @returns {Number} The quadratic attenuation factor. */ get quadraticAttenuation() { return this._state.attenuation[2]; } /** * Sets if this PointLight casts a shadow. * * Default value is ````false````. * * @param {Boolean} castsShadow Set ````true```` to cast shadows. */ set castsShadow(castsShadow) { castsShadow = !!castsShadow; if (this._state.castsShadow === castsShadow) { return; } this._state.castsShadow = castsShadow; this._shadowViewMatrixDirty = true; this.glRedraw(); } /** * Gets if this PointLight casts a shadow. * * Default value is ````false````. * * @returns {Boolean} ````true```` if this PointLight casts shadows. */ get castsShadow() { return this._state.castsShadow; } /** * Destroys this PointLight. */ destroy() { const camera = this.scene.camera; const canvas = this.scene.canvas; camera.off(this._onCameraViewMatrix); camera.off(this._onCameraProjMatrix); canvas.off(this._onCanvasBoundary); super.destroy(); this._state.destroy(); if (this._shadowRenderBuf) { this._shadowRenderBuf.destroy(); } this.scene._lightDestroyed(this); this.glRedraw(); } } function ensureImageSizePowerOfTwo(image) { if (!isPowerOfTwo(image.width) || !isPowerOfTwo(image.height)) { const canvas = document.createElement("canvas"); canvas.width = nextHighestPowerOfTwo(image.width); canvas.height = nextHighestPowerOfTwo(image.height); const ctx = canvas.getContext("2d"); ctx.drawImage(image, 0, 0, image.width, image.height, 0, 0, canvas.width, canvas.height); image = canvas; } return image; } function isPowerOfTwo(x) { return (x & (x - 1)) === 0; } function nextHighestPowerOfTwo(x) { --x; for (let i = 1; i < 32; i <<= 1) { x = x | x >> i; } return x + 1; } /** * @desc A cube texture map. */ class CubeTexture extends Component { /** @private */ get type() { return "CubeTexture"; } /** * @constructor * @param {Component} owner Owner component. When destroyed, the owner will destroy this component as well. * @param {*} [cfg] Configs * @param {String} [cfg.id] Optional ID for this CubeTexture, unique among all components in the parent scene, generated automatically when omitted. * @param {String[]} [cfg.src=null] Paths to six image files to load into this CubeTexture. * @param {Boolean} [cfg.flipY=false] Flips this CubeTexture's source data along its vertical axis when true. * @param {Number} [cfg.encoding=LinearEncoding] Encoding format. Supported values are {@link LinearEncoding} and {@link sRGBEncoding}. */ constructor(owner, cfg = {}) { super(owner, cfg); const gl = this.scene.canvas.gl; this._state = new RenderState({ texture: new Texture2D({gl, target: gl.TEXTURE_CUBE_MAP}), flipY: this._checkFlipY(cfg.minFilter), encoding: this._checkEncoding(cfg.encoding), minFilter: LinearMipmapLinearFilter, magFilter: LinearFilter, wrapS: ClampToEdgeWrapping, wrapT: ClampToEdgeWrapping, mipmaps: true }); this._src = cfg.src; this._images = []; this._loadSrc(cfg.src); stats.memory.textures++; } _checkFlipY(value) { return !!value; } _checkEncoding(value) { value = value || LinearEncoding; if (value !== LinearEncoding && value !== sRGBEncoding) { this.error("Unsupported value for 'encoding' - supported values are LinearEncoding and sRGBEncoding. Defaulting to LinearEncoding."); value = LinearEncoding; } return value; } _webglContextRestored() { this.scene.canvas.gl; this._state.texture = null; // if (this._images.length > 0) { // this._state.texture = new xeokit.renderer.Texture2D(gl, gl.TEXTURE_CUBE_MAP); // this._state.texture.setImage(this._images, this._state); // this._state.texture.setProps(this._state); // } else if (this._src) { this._loadSrc(this._src); } } _loadSrc(src) { const self = this; const gl = this.scene.canvas.gl; this._images = []; let loadFailed = false; let numLoaded = 0; for (let i = 0; i < src.length; i++) { const image = new Image(); image.onload = (function () { let _image = image; const index = i; return function () { if (loadFailed) { return; } _image = ensureImageSizePowerOfTwo(_image); self._images[index] = _image; numLoaded++; if (numLoaded === 6) { let texture = self._state.texture; if (!texture) { texture = new Texture2D({gl, target: gl.TEXTURE_CUBE_MAP}); self._state.texture = texture; } texture.setImage(self._images, self._state); self.fire("loaded", self._src, false); self.glRedraw(); } }; })(); image.onerror = function () { loadFailed = true; }; image.src = src[i]; } } /** * Destroys this CubeTexture * */ destroy() { super.destroy(); if (this._state.texture) { this._state.texture.destroy(); } stats.memory.textures--; this._state.destroy(); } } /** * @desc A reflection cube map. * * ## Usage * * ````javascript * import {Viewer, Mesh, buildSphereGeometry, * ReadableGeometry, MetallicMaterial, ReflectionMap} from "xeokit-sdk.es.js"; * * // Create a Viewer and arrange the camera * * const viewer = new Viewer({ * canvasId: "myCanvas" * }); * * viewer.scene.camera.eye = [0, 0, 5]; * viewer.scene.camera.look = [0, 0, 0]; * viewer.scene.camera.up = [0, 1, 0]; * * new ReflectionMap(viewer.scene, { * src: [ * "textures/reflect/Uffizi_Gallery/Uffizi_Gallery_Radiance_PX.png", * "textures/reflect/Uffizi_Gallery/Uffizi_Gallery_Radiance_NX.png", * "textures/reflect/Uffizi_Gallery/Uffizi_Gallery_Radiance_PY.png", * "textures/reflect/Uffizi_Gallery/Uffizi_Gallery_Radiance_NY.png", * "textures/reflect/Uffizi_Gallery/Uffizi_Gallery_Radiance_PZ.png", * "textures/reflect/Uffizi_Gallery/Uffizi_Gallery_Radiance_NZ.png" * ] * }); * * // Create a sphere and ground plane * * new Mesh(viewer.scene, { * geometry: new ReadableGeometry(viewer.scene, buildSphereGeometry({ * radius: 2.0 * }), * new MetallicMaterial(viewer.scene, { * baseColor: [1, 1, 1], * metallic: 1.0, * roughness: 1.0 * }) * }); * ```` */ class ReflectionMap extends CubeTexture { /** @private */ get type() { return "ReflectionMap"; } /** * @param {Component} owner Owner component. When destroyed, the owner will destroy this component as well. * @param {*} [cfg] Configs * @param {String} [cfg.id] Optional ID for this ReflectionMap, unique among all components in the parent scene, generated automatically when omitted. * @param {String[]} [cfg.src=null] Paths to six image files to load into this ReflectionMap. * @param {Boolean} [cfg.flipY=false] Flips this ReflectionMap's source data along its vertical axis when true. * @param {Number} [cfg.encoding=LinearEncoding] Encoding format. Supported values are {@link LinearEncoding} and {@link sRGBEncoding}. */ constructor(owner, cfg = {}) { super(owner, cfg); this.scene._lightsState.addReflectionMap(this._state); this.scene._reflectionMapCreated(this); } /** * Destroys this ReflectionMap. */ destroy() { super.destroy(); this.scene._reflectionMapDestroyed(this); } } /** * @desc A **LightMap** specifies a cube texture light map. * * ## Usage * * ````javascript * import {Viewer, Mesh, buildSphereGeometry, * ReadableGeometry, MetallicMaterial, LightMap} from "xeokit-sdk.es.js"; * * // Create a Viewer and arrange the camera * * const viewer = new Viewer({ * canvasId: "myCanvas" * }); * * viewer.scene.camera.eye = [0, 0, 5]; * viewer.scene.camera.look = [0, 0, 0]; * viewer.scene.camera.up = [0, 1, 0]; * * new LightMap(viewer.scene, { * src: [ * "textures/light/Uffizi_Gallery/Uffizi_Gallery_Irradiance_PX.png", * "textures/light/Uffizi_Gallery/Uffizi_Gallery_Irradiance_NX.png", * "textures/light/Uffizi_Gallery/Uffizi_Gallery_Irradiance_PY.png", * "textures/light/Uffizi_Gallery/Uffizi_Gallery_Irradiance_NY.png", * "textures/light/Uffizi_Gallery/Uffizi_Gallery_Irradiance_PZ.png", * "textures/light/Uffizi_Gallery/Uffizi_Gallery_Irradiance_NZ.png" * ] * }); * * // Create a sphere and ground plane * * new Mesh(viewer.scene, { * geometry: new ReadableGeometry(viewer.scene, buildSphereGeometry({ * radius: 2.0 * }), * new MetallicMaterial(viewer.scene, { * baseColor: [1, 1, 1], * metallic: 1.0, * roughness: 1.0 * }) * }); * ```` */ class LightMap extends CubeTexture { /** @private */ get type() { return "LightMap"; } /** * @constructor * @param {Component} owner Owner component. When destroyed, the owner will destroy this component as well. * @param {*} [cfg] Configs * @param {String} [cfg.id] Optional ID for this LightMap, unique among all components in the parent scene, generated automatically when omitted. * @param {String:Object} [cfg.meta] Optional map of user-defined metadata to attach to this LightMap. * @param {String[]} [cfg.src=null] Paths to six image files to load into this LightMap. * @param {Boolean} [cfg.flipY=false] Flips this LightMap's source data along its vertical axis when true. * @param {Number} [cfg.encoding=LinearEncoding] Encoding format. Supported values are {@link LinearEncoding} and {@link sRGBEncoding}. */ constructor(owner, cfg = {}) { super(owner, cfg); this.scene._lightMapCreated(this); } destroy() { super.destroy(); this.scene._lightMapDestroyed(this); } } const tempVec4a$9 = math.vec4(); const tempVec4b$8 = math.vec4(); /** * @desc Tracks the World, View and Canvas coordinates, and visibility, of a position within a {@link Scene}. * * ## Position * * A Marker holds its position in the World, View and Canvas coordinate systems in three properties: * * * {@link Marker#worldPos} holds the Marker's 3D World-space coordinates. This property can be dynamically updated. The Marker will fire a "worldPos" event whenever this property changes. * * {@link Marker#viewPos} holds the Marker's 3D View-space coordinates. This property is read-only, and is automatically updated from {@link Marker#worldPos} and the current {@link Camera} position. The Marker will fire a "viewPos" event whenever this property changes. * * {@link Marker#canvasPos} holds the Marker's 2D Canvas-space coordinates. This property is read-only, and is automatically updated from {@link Marker#canvasPos} and the current {@link Camera} position and projection. The Marker will fire a "canvasPos" event whenever this property changes. * * ## Visibility * * {@link Marker#visible} indicates if the Marker is currently visible. The Marker will fire a "visible" event whenever {@link Marker#visible} changes. * * This property will be ````false```` when: * * * {@link Marker#entity} is set to an {@link Entity}, and {@link Entity#visible} is ````false````, * * {@link Marker#occludable} is ````true```` and the Marker is occluded by some {@link Entity} in the 3D view, or * * {@link Marker#canvasPos} is outside the boundary of the {@link Canvas}. * * ## Usage * * In the example below, we'll create a Marker that's associated with a {@link Mesh} (which a type of {@link Entity}). * * We'll configure our Marker to * become invisible whenever it's occluded by any Entities in the canvas. * * We'll also demonstrate how to query the Marker's visibility status and position (in the World, View and * Canvas coordinate systems), and how to subscribe to change events on those properties. * * ````javascript * import {Viewer, GLTFLoaderPlugin, Marker} from "xeokit-sdk.es.js"; * * const viewer = new Viewer({ * canvasId: "myCanvas" * }); * * // Create the torus Mesh * // Recall that a Mesh is an Entity * new Mesh(viewer.scene, { * geometry: new ReadableGeometry(viewer.scene, buildTorusGeometry({ * center: [0,0,0], * radius: 1.0, * tube: 0.5, * radialSegments: 32, * tubeSegments: 24, * arc: Math.PI * 2.0 * }), * material: new PhongMaterial(viewer.scene, { * diffuseMap: new Texture(viewer.scene, { * src: "textures/diffuse/uvGrid2.jpg" * }), * backfaces: true * }) * }); * * // Create the Marker, associated with our Mesh Entity * const myMarker = new Marker(viewer, { * entity: entity, * worldPos: [10,0,0], * occludable: true * }); * * // Get the Marker's current World, View and Canvas coordinates * const worldPos = myMarker.worldPos; // 3D World-space position * const viewPos = myMarker.viewPos; // 3D View-space position * const canvasPos = myMarker.canvasPos; // 2D Canvas-space position * * const visible = myMarker.visible; * * // Listen for change of the Marker's 3D World-space position * myMarker.on("worldPos", function(worldPos) { * //... * }); * * // Listen for change of the Marker's 3D View-space position, which happens * // when either worldPos was updated or the Camera was moved * myMarker.on("viewPos", function(viewPos) { * //... * }); * * // Listen for change of the Marker's 2D Canvas-space position, which happens * // when worldPos or viewPos was updated, or Camera's projection was updated * myMarker.on("canvasPos", function(canvasPos) { * //... * }); * * // Listen for change of Marker visibility. The Marker becomes invisible when it falls outside the canvas, * // has an Entity that is also invisible, or when an Entity occludes the Marker's position in the 3D view. * myMarker.on("visible", function(visible) { // Marker visibility has changed * if (visible) { * this.log("Marker is visible"); * } else { * this.log("Marker is invisible"); * } * }); * * // Listen for destruction of Marker * myMarker.on("destroyed", () => { * //... * }); * ```` */ class Marker extends Component { /** * @constructor * @param {Component} [owner] Owner component. When destroyed, the owner will destroy this Marker as well. * @param {*} [cfg] Marker configuration * @param {String} [cfg.id] Optional ID, unique among all components in the parent {@link Scene}, generated automatically when omitted. * @param {Entity} [cfg.entity] Entity to associate this Marker with. When the Marker has an Entity, then {@link Marker#visible} will always be ````false```` if {@link Entity#visible} is false. * @param {Boolean} [cfg.occludable=false] Indicates whether or not this Marker is hidden (ie. {@link Marker#visible} is ````false```` whenever occluded by {@link Entity}s in the {@link Scene}. * @param {Number[]} [cfg.worldPos=[0,0,0]] World-space 3D Marker position. */ constructor(owner, cfg) { super(owner, cfg); this._entity = null; this._visible = null; this._worldPos = math.vec3(); this._origin = math.vec3(); this._rtcPos = math.vec3(); this._viewPos = math.vec3(); this._canvasPos = math.vec2(); this._occludable = false; this._onCameraViewMatrix = this.scene.camera.on("matrix", () => { this._viewPosDirty = true; this._needUpdate(); }); this._onCameraProjMatrix = this.scene.camera.on("projMatrix", () => { this._canvasPosDirty = true; this._needUpdate(); }); this._onEntityDestroyed = null; this._onEntityModelDestroyed = null; this._renderer.addMarker(this); this.entity = cfg.entity; this.worldPos = cfg.worldPos; this.occludable = cfg.occludable; } _update() { // this._needUpdate() schedules this for next tick if (this._viewPosDirty) { math.transformPoint3(this.scene.camera.viewMatrix, this._worldPos, this._viewPos); this._viewPosDirty = false; this._canvasPosDirty = true; this.fire("viewPos", this._viewPos); } if (this._canvasPosDirty) { tempVec4a$9.set(this._viewPos); tempVec4a$9[3] = 1.0; math.transformPoint4(this.scene.camera.projMatrix, tempVec4a$9, tempVec4b$8); const aabb = this.scene.canvas.boundary; this._canvasPos[0] = Math.floor((1 + tempVec4b$8[0] / tempVec4b$8[3]) * aabb[2] / 2); this._canvasPos[1] = Math.floor((1 - tempVec4b$8[1] / tempVec4b$8[3]) * aabb[3] / 2); this._canvasPosDirty = false; this.fire("canvasPos", this._canvasPos); } } _setVisible(visible) { // Called by VisibilityTester and this._entity.on("destroyed"..) if (this._visible === visible) ; this._visible = visible; this.fire("visible", this._visible); } /** * Sets the {@link Entity} this Marker is associated with. * * An Entity is optional. When the Marker has an Entity, then {@link Marker#visible} will always be ````false```` * if {@link Entity#visible} is false. * * @type {Entity} */ set entity(entity) { if (this._entity === entity) { return; } this._cleanupDestroyedHandlers(); this._entity = entity; if (this._entity) { if (this._entity.isSceneModelEntity) { this._onEntityModelDestroyed = this._entity.model.on("destroyed", () => { // SceneModelEntity does not fire events, and cannot exist beyond its VBOSceneModel this._entity = null; // Marker now may become visible, if it was synched to invisible Entity this._onEntityModelDestroyed = null; }); } else { this._onEntityDestroyed = this._entity.on("destroyed", () => { this._entity = null; this._onEntityDestroyed = null; }); } } this.fire("entity", this._entity, true /* forget */); } /** * Gets the {@link Entity} this Marker is associated with. * * @type {Entity} */ get entity() { return this._entity; } /** * Sets whether occlusion testing is performed for this Marker. * * When this is ````true````, then {@link Marker#visible} will be ````false```` whenever the Marker is occluded by an {@link Entity} in the 3D view. * * The {@link Scene} periodically occlusion-tests all Markers on every 20th "tick" (which represents a rendered frame). We * can adjust that frequency via property {@link Scene#ticksPerOcclusionTest}. * * @type {Boolean} */ set occludable(occludable) { occludable = !!occludable; if (occludable === this._occludable) { return; } this._occludable = occludable; if (this._occludable) { this._renderer.markerWorldPosUpdated(this); } } /** * Gets whether occlusion testing is performed for this Marker. * * When this is ````true````, then {@link Marker#visible} will be ````false```` whenever the Marker is occluded by an {@link Entity} in the 3D view. * * @type {Boolean} */ get occludable() { return this._occludable; } /** * Sets the World-space 3D position of this Marker. * * Fires a "worldPos" event with new World position. * * @type {Number[]} */ set worldPos(worldPos) { this._worldPos.set(worldPos || [0, 0, 0]); worldToRTCPos(this._worldPos, this._origin, this._rtcPos); if (this._occludable) { this._renderer.markerWorldPosUpdated(this); } this._viewPosDirty = true; this.fire("worldPos", this._worldPos); this._needUpdate(); } /** * Gets the World-space 3D position of this Marker. * * @type {Number[]} */ get worldPos() { return this._worldPos; } /** * Gets the RTC center of this Marker. * * This is automatically calculated from {@link Marker#worldPos}. * * @type {Number[]} */ get origin() { return this._origin; } /** * Gets the RTC position of this Marker. * * This is automatically calculated from {@link Marker#worldPos}. * * @type {Number[]} */ get rtcPos() { return this._rtcPos; } /** * View-space 3D coordinates of this Marker. * * This property is read-only and is automatically calculated from {@link Marker#worldPos} and the current {@link Camera} position. * * The Marker fires a "viewPos" event whenever this property changes. * * @type {Number[]} * @final */ get viewPos() { this._update(); return this._viewPos; } /** * Canvas-space 2D coordinates of this Marker. * * This property is read-only and is automatically calculated from {@link Marker#worldPos} and the current {@link Camera} position and projection. * * The Marker fires a "canvasPos" event whenever this property changes. * * @type {Number[]} * @final */ get canvasPos() { this._update(); return this._canvasPos; } /** * Indicates if this Marker is currently visible. * * This is read-only and is automatically calculated. * * The Marker is **invisible** whenever: * * * {@link Marker#canvasPos} is currently outside the canvas, * * {@link Marker#entity} is set to an {@link Entity} that has {@link Entity#visible} ````false````, or * * or {@link Marker#occludable} is ````true```` and the Marker is currently occluded by an Entity in the 3D view. * * The Marker fires a "visible" event whenever this property changes. * * @type {Boolean} * @final */ get visible() { return !!this._visible; } /** * Destroys this Marker. */ destroy() { this.fire("destroyed", true); this.scene.camera.off(this._onCameraViewMatrix); this.scene.camera.off(this._onCameraProjMatrix); this._cleanupDestroyedHandlers(); this._renderer.removeMarker(this); super.destroy(); } _cleanupDestroyedHandlers() { if (this._entity) { if (this._onEntityDestroyed !== null) { if (this._entity.model) { this._entity.model.off(this._onEntityDestroyed); } else { this._entity.off(this._onEntityDestroyed); } this._onEntityDestroyed = null; } if (this._onEntityModelDestroyed !== null) { if (this._entity.model) { this._entity.model.off(this._onEntityModelDestroyed); } this._onEntityModelDestroyed = null; } } } } /** * A {@link Marker} with a billboarded and textured quad attached to it. * * * Extends {@link Marker} * * Keeps the quad oriented towards the viewpoint * * Auto-fits the quad to the texture * * Has a world-space position * * Can be configured to hide the quad whenever the position is occluded by some other object * * ## Usage * * [[Run this example](/examples/index.html#markers_SpriteMarker)] * * ```` javascript * import {Viewer, SpriteMarker } from "./https://cdn.jsdelivr.net/npm/@xeokit/xeokit-sdk/dist/xeokit-sdk.es.min.js"; * * const viewer = new Viewer({ * canvasId: "myCanvas", * transparent: true * }); * * viewer.scene.camera.eye = [0, 0, 25]; * viewer.scene.camera.look = [0, 0, 0]; * viewer.scene.camera.up = [0, 1, 0]; * * new SpriteMarker(viewer.scene, { * worldPos: [-10, 0, 0], * src: "../assets/textures/diffuse/uvGrid2_512x1024.jpg", * size: 5, * occludable: false * }); * * new SpriteMarker(viewer.scene, { * worldPos: [+10, 0, 0], * src: "../assets/textures/diffuse/uvGrid2_1024x512.jpg", * size: 4, * occludable: false * }); *```` */ class SpriteMarker extends Marker { /** * @constructor * @param {Component} owner Owner component. When destroyed, the owner will destroy this SpriteMarker as well. * @param {*} [cfg] Configs * @param {String} [cfg.id] Optional ID for this SpriteMarker, unique among all components in the parent scene, generated automatically when omitted. * @param {Entity} [cfg.entity] Entity to associate this Marker with. When the SpriteMarker has an Entity, then {@link Marker#visible} will always be ````false```` if {@link Entity#visible} is false. * @param {Boolean} [cfg.occludable=false] Indicates whether or not this Marker is hidden (ie. {@link Marker#visible} is ````false```` whenever occluded by {@link Entity}s in the {@link Scene}. * @param {Number[]} [cfg.worldPos=[0,0,0]] World-space 3D Marker position. * @param {String} [cfg.src=null] Path to image file to load into this SpriteMarker. See the {@link SpriteMarker#src} property for more info. * @param {HTMLImageElement} [cfg.image=null] HTML Image object to load into this SpriteMarker. See the {@link SpriteMarker#image} property for more info. * @param {Boolean} [cfg.flipY=false] Flips this SpriteMarker's texture image along its vertical axis when true. * @param {String} [cfg.encoding="linear"] Texture encoding format. See the {@link Texture#encoding} property for more info. */ constructor(owner, cfg = {}) { super(owner, { entity: cfg.entity, occludable: cfg.occludable, worldPos: cfg.worldPos }); this._occluded = false; this._visible = true; this._src = null; this._image = null; this._pos = math.vec3(); this._origin = math.vec3(); this._rtcPos = math.vec3(); this._dir = math.vec3(); this._size = 1.0; this._imageSize = math.vec2(); this._texture = new Texture(this, { src: cfg.src }); this._geometry = new ReadableGeometry(this, { primitive: "triangles", positions: [3, 3, 0, -3, 3, 0, -3, -3, 0, 3, -3, 0], normals: [-1, 0, 0, -1, 0, 0, -1, 0, 0, -1, 0, 0], uv: [1, -1, 0, -1, 0, 0, 1, 0], indices: [0, 1, 2, 0, 2, 3] // Ensure these will be front-faces }); this._mesh = new Mesh(this, { geometry: this._geometry, material: new PhongMaterial(this, { ambient: [0.9, 0.3, 0.9], shininess: 30, diffuseMap: this._texture, backfaces: true }), scale: [1, 1, 1], // Note: by design, scale does not work with billboard position: cfg.worldPos, rotation: [90, 0, 0], billboard: "spherical", occluder: false // Don't occlude SpriteMarkers or Annotations }); this.visible = true; this.collidable = cfg.collidable; this.clippable = cfg.clippable; this.pickable = cfg.pickable; this.opacity = cfg.opacity; this.size = cfg.size; if (cfg.image) { this.image = cfg.image; } else { this.src = cfg.src; } } _setVisible(visible) { // Called by VisibilityTester and this._entity.on("destroyed"..) this._occluded = (!visible); this._mesh.visible = this._visible && (!this._occluded); super._setVisible(visible); } /** * Sets if this ````SpriteMarker```` is visible or not. * * Default value is ````true````. * * @param {Boolean} visible Set ````true```` to make this ````SpriteMarker```` visible. */ set visible(visible) { this._visible = (visible === null || visible === undefined) ? true : visible; this._mesh.visible = this._visible && (!this._occluded); } /** * Gets if this ````SpriteMarker```` is visible or not. * * Default value is ````true````. * * @returns {Boolean} Returns ````true```` if visible. */ get visible() { return this._visible; } /** * Sets an ````HTMLImageElement```` to source the image from. * * Sets {@link Texture#src} null. * * @type {HTMLImageElement} */ set image(image) { this._image = image; if (this._image) { this._imageSize[0] = this._image.width; this._imageSize[1] = this._image.height; this._updatePlaneSizeFromImage(); this._src = null; this._texture.image = this._image; } } /** * Gets the ````HTMLImageElement```` the ````SpriteMarker````'s image is sourced from, if set. * * Returns null if not set. * * @type {HTMLImageElement} */ get image() { return this._image; } /** * Sets an image file path that the ````SpriteMarker````'s image is sourced from. * * Accepted file types are PNG and JPEG. * * Sets {@link Texture#image} null. * * @type {String} */ set src(src) { this._src = src; if (this._src) { this._image = null; const image = new Image(); image.onload = () => { this._texture.image = image; this._imageSize[0] = image.width; this._imageSize[1] = image.height; this._updatePlaneSizeFromImage(); }; image.src = this._src; } } /** * Gets the image file path that the ````SpriteMarker````'s image is sourced from, if set. * * Returns null if not set. * * @type {String} */ get src() { return this._src; } /** * Sets the World-space size of the longest edge of the ````SpriteMarker````. * * Note that ````SpriteMarker```` sets its aspect ratio to match its image. If we set a value of ````1000````, and * the image has size ````400x300````, then the ````SpriteMarker```` will then have size ````1000 x 750````. * * Default value is ````1.0````. * * @param {Number} size New World-space size of the ````SpriteMarker````. */ set size(size) { this._size = (size === undefined || size === null) ? 1.0 : size; if (this._image) { this._updatePlaneSizeFromImage(); } } /** * Gets the World-space size of the longest edge of the ````SpriteMarker````. * * Returns {Number} World-space size of the ````SpriteMarker````. */ get size() { return this._size; } /** * Sets if this ````SpriteMarker```` is included in boundary calculations. * * Default is ````true````. * * @type {Boolean} */ set collidable(value) { this._mesh.collidable = (value !== false); } /** * Gets if this ````SpriteMarker```` is included in boundary calculations. * * Default is ````true````. * * @type {Boolean} */ get collidable() { return this._mesh.collidable; } /** * Sets if this ````SpriteMarker```` is clippable. * * Clipping is done by the {@link SectionPlane}s in {@link Scene#sectionPlanes}. * * Default is ````true````. * * @type {Boolean} */ set clippable(value) { this._mesh.clippable = (value !== false); } /** * Gets if this ````SpriteMarker```` is clippable. * * Clipping is done by the {@link SectionPlane}s in {@link Scene#sectionPlanes}. * * Default is ````true````. * * @type {Boolean} */ get clippable() { return this._mesh.clippable; } /** * Sets if this ````SpriteMarker```` is pickable. * * Default is ````true````. * * @type {Boolean} */ set pickable(value) { this._mesh.pickable = (value !== false); } /** * Gets if this ````SpriteMarker```` is pickable. * * Default is ````true````. * * @type {Boolean} */ get pickable() { return this._mesh.pickable; } /** * Sets the opacity factor for this ````SpriteMarker````. * * This is a factor in range ````[0..1]```` which multiplies by the rendered fragment alphas. * * @type {Number} */ set opacity(opacity) { this._mesh.opacity = opacity; } /** * Gets this ````SpriteMarker````'s opacity factor. * * This is a factor in range ````[0..1]```` which multiplies by the rendered fragment alphas. * * @type {Number} */ get opacity() { return this._mesh.opacity; } _updatePlaneSizeFromImage() { const halfSize = this._size * 0.5; const width = this._imageSize[0]; const height = this._imageSize[1]; const aspect = height / width; if (width > height) { this._geometry.positions = [ halfSize, halfSize * aspect, 0, -halfSize, halfSize * aspect, 0, -halfSize, -halfSize * aspect, 0, halfSize, -halfSize * aspect, 0 ]; } else { this._geometry.positions = [ halfSize / aspect, halfSize, 0, -halfSize / aspect, halfSize, 0, -halfSize / aspect, -halfSize, 0, halfSize / aspect, -halfSize, 0 ]; } } } /** * @desc Saves and restores the state of a {@link Scene}'s {@link Camera}. * * ## See Also * * * {@link ModelMemento} - Saves and restores a snapshot of the visual state of the {@link Entity}'s of a model within a {@link Scene}. * * {@link ObjectsMemento} - Saves and restores a snapshot of the visual state of the {@link Entity}'s that represent objects within a {@link Scene}. * * ## Usage * * In the example below, we'll create a {@link Viewer} and use an {@link XKTLoaderPlugin} to load an ````.xkt```` model. When the model has loaded, we'll save a snapshot of the {@link Camera} state in an CameraMemento. Then we'll move the Camera, and then we'll restore its original state again from the CameraMemento. * * ````javascript * import {Viewer, XKTLoaderPlugin, CameraMemento} from "xeokit-sdk.es.js"; * * const viewer = new Viewer({ * canvasId: "myCanvas" * }); * * // Load a model * const xktLoader = new XKTLoaderPlugin(viewer); * * const model = xktLoader.load({ * id: "myModel", * src: "./models/xkt/schependomlaan/schependomlaan.xkt" * }); * * // Set camera * viewer.camera.eye = [-2.56, 8.38, 8.27]; * viewer.camera.look = [13.44, 3.31, -14.83]; * viewer.camera.up = [0.10, 0.98, -0.14]; * * model.on("loaded", () => { * * // Model has loaded * * // Save memento of camera state * const cameraMemento = new CameraMemento(); * * cameraMemento.saveCamera(viewer.scene); * * // Move the camera * viewer.camera.eye = [45.3, 2.00, 5.13]; * viewer.camera.look = [0.0, 5.5, 10.0]; * viewer.camera.up = [0.10, 0.98, -0.14]; * * // Restore the camera state again * objectsMemento.restoreCamera(viewer.scene); * }); * ```` */ class CameraMemento { /** * Creates a CameraState. * * @param {Scene} [scene] When given, immediately saves the state of the given {@link Scene}'s {@link Camera}. */ constructor(scene) { /** @private */ this._eye = math.vec3(); /** @private */ this._look = math.vec3(); /** @private */ this._up = math.vec3(); /** @private */ this._projection = {}; if (scene) { this.saveCamera(scene); } } /** * Saves the state of the given {@link Scene}'s {@link Camera}. * * @param {Scene} scene The scene that contains the {@link Camera}. */ saveCamera(scene) { const camera = scene.camera; const project = camera.project; this._eye.set(camera.eye); this._look.set(camera.look); this._up.set(camera.up); switch (camera.projection) { case "perspective": this._projection = { projection: "perspective", fov: project.fov, fovAxis: project.fovAxis, near: project.near, far: project.far }; break; case "ortho": this._projection = { projection: "ortho", scale: project.scale, near: project.near, far: project.far }; break; case "frustum": this._projection = { projection: "frustum", left: project.left, right: project.right, top: project.top, bottom: project.bottom, near: project.near, far: project.far }; break; case "custom": this._projection = { projection: "custom", matrix: project.matrix.slice() }; break; } } /** * Restores a {@link Scene}'s {@link Camera} to the state previously captured with {@link CameraMemento#saveCamera}. * * @param {Scene} scene The scene. * @param {Function} [done] When this callback is given, will fly the {@link Camera} to the saved state then fire the callback. Otherwise will just jump the Camera to the saved state. */ restoreCamera(scene, done) { const camera = scene.camera; const savedProjection = this._projection; function restoreProjection() { switch (savedProjection.type) { case "perspective": camera.perspective.fov = savedProjection.fov; camera.perspective.fovAxis = savedProjection.fovAxis; camera.perspective.near = savedProjection.near; camera.perspective.far = savedProjection.far; break; case "ortho": camera.ortho.scale = savedProjection.scale; camera.ortho.near = savedProjection.near; camera.ortho.far = savedProjection.far; break; case "frustum": camera.frustum.left = savedProjection.left; camera.frustum.right = savedProjection.right; camera.frustum.top = savedProjection.top; camera.frustum.bottom = savedProjection.bottom; camera.frustum.near = savedProjection.near; camera.frustum.far = savedProjection.far; break; case "custom": camera.customProjection.matrix = savedProjection.matrix; break; } } if (done) { scene.viewer.cameraFlight.flyTo({ eye: this._eye, look: this._look, up: this._up, orthoScale: savedProjection.scale, projection: savedProjection.projection }, () => { restoreProjection(); done(); }); } else { camera.eye = this._eye; camera.look = this._look; camera.up = this._up; restoreProjection(); camera.projection = savedProjection.projection; } } } const color$3 = math.vec3(); /** * @desc Saves and restores a snapshot of the visual state of the {@link Entity}'s of a model within a {@link Scene}. * * ## Usage * * In the example below, we'll create a {@link Viewer} and use an {@link XKTLoaderPlugin} to load an ````.xkt```` model. When the model has loaded, we'll hide a couple of {@link Entity}s and save a snapshot of the visual states of all its Entitys in an ModelMemento. Then we'll show all the Entitys * again, and then we'll restore the visual states of all the Entitys again from the ModelMemento, which will hide those two Entitys again. * * ## See Also * * * {@link CameraMemento} - Saves and restores the state of a {@link Scene}'s {@link Camera}. * * {@link ObjectsMemento} - Saves and restores a snapshot of the visual state of the {@link Entity}'s that represent objects within a {@link Scene}. * * ````javascript * import {Viewer, XKTLoaderPlugin, ModelMemento} from "xeokit-sdk.es.js"; * * const viewer = new Viewer({ * canvasId: "myCanvas" * }); * * // Load a model * const xktLoader = new XKTLoaderPlugin(viewer); * * const model = xktLoader.load({ * id: "myModel", * src: "./models/xkt/schependomlaan/schependomlaan.xkt" * }); * * model.on("loaded", () => { * * // Model has loaded * * // Hide a couple of objects * viewer.scene.objects["0u4wgLe6n0ABVaiXyikbkA"].visible = false; * viewer.scene.objects["3u4wgLe3n0AXVaiXyikbYO"].visible = false; * * // Save memento of all object states, which includes those two hidden objects * const ModelMemento = new ModelMemento(); * * const metaModel = viewer.metaScene.metaModels * ModelMemento.saveObjects(viewer.scene); * * // Show all objects * viewer.scene.setObjectsVisible(viewer.scene.objectIds, true); * * // Restore the objects states again, which involves hiding those two objects again * ModelMemento.restoreObjects(viewer.scene); * }); * ````` * * ## Masking Saved State * * We can optionally supply a mask to focus what state we save and restore. * * For example, to save and restore only the {@link Entity#visible} and {@link Entity#clippable} states: * * ````javascript * ModelMemento.saveObjects(viewer.scene, { * visible: true, * clippable: true * }); * * //... * * // Restore the objects states again * ModelMemento.restoreObjects(viewer.scene); * ```` */ class ModelMemento { /** * Creates a ModelMemento. * * @param {MetaModel} [metaModel] When given, immediately saves the model's {@link Entity} states to this ModelMemento. */ constructor(metaModel) { /** @private */ this.objectsVisible = []; /** @private */ this.objectsEdges = []; /** @private */ this.objectsXrayed = []; /** @private */ this.objectsHighlighted = []; /** @private */ this.objectsSelected = []; /** @private */ this.objectsClippable = []; /** @private */ this.objectsPickable = []; /** @private */ this.objectsColorize = []; /** @private */ this.objectsOpacity = []; /** @private */ this.numObjects = 0; if (metaModel) { const metaScene = metaModel.metaScene; const scene = metaScene.scene; this.saveObjects(scene, metaModel); } } /** * Saves a snapshot of the visual state of the {@link Entity}'s that represent objects within a model. * * @param {Scene} scene The scene. * @param {MetaModel} metaModel Represents the model. Corresponds with an {@link Entity} that represents the model in the scene. * @param {Object} [mask] Masks what state gets saved. Saves all state when not supplied. * @param {boolean} [mask.visible] Saves {@link Entity#visible} values when ````true````. * @param {boolean} [mask.visible] Saves {@link Entity#visible} values when ````true````. * @param {boolean} [mask.edges] Saves {@link Entity#edges} values when ````true````. * @param {boolean} [mask.xrayed] Saves {@link Entity#xrayed} values when ````true````. * @param {boolean} [mask.highlighted] Saves {@link Entity#highlighted} values when ````true````. * @param {boolean} [mask.selected] Saves {@link Entity#selected} values when ````true````. * @param {boolean} [mask.clippable] Saves {@link Entity#clippable} values when ````true````. * @param {boolean} [mask.pickable] Saves {@link Entity#pickable} values when ````true````. * @param {boolean} [mask.colorize] Saves {@link Entity#colorize} values when ````true````. * @param {boolean} [mask.opacity] Saves {@link Entity#opacity} values when ````true````. */ saveObjects(scene, metaModel, mask) { this.numObjects = 0; this._mask = mask ? utils.apply(mask, {}) : null; const visible = (!mask || mask.visible); const edges = (!mask || mask.edges); const xrayed = (!mask || mask.xrayed); const highlighted = (!mask || mask.highlighted); const selected = (!mask || mask.selected); const clippable = (!mask || mask.clippable); const pickable = (!mask || mask.pickable); const colorize = (!mask || mask.colorize); const opacity = (!mask || mask.opacity); const metaObjects = metaModel.metaObjects; const objects = scene.objects; for (let i = 0, len = metaObjects.length; i < len; i++) { const metaObject = metaObjects[i]; const objectId = metaObject.id; const object = objects[objectId]; if (!object) { continue; } if (visible) { this.objectsVisible[i] = object.visible; } if (edges) { this.objectsEdges[i] = object.edges; } if (xrayed) { this.objectsXrayed[i] = object.xrayed; } if (highlighted) { this.objectsHighlighted[i] = object.highlighted; } if (selected) { this.objectsSelected[i] = object.selected; } if (clippable) { this.objectsClippable[i] = object.clippable; } if (pickable) { this.objectsPickable[i] = object.pickable; } if (colorize) { const objectColor = object.colorize; this.objectsColorize[i * 3 + 0] = objectColor[0]; this.objectsColorize[i * 3 + 1] = objectColor[1]; this.objectsColorize[i * 3 + 2] = objectColor[2]; } if (opacity) { this.objectsOpacity[i] = object.opacity; } this.numObjects++; } } /** * Restores a {@link Scene}'s {@link Entity}'s to their state previously captured with {@link ModelMemento#saveObjects}. * * Assumes that the model has not been destroyed or modified since saving. * * @param {Scene} scene The scene that was given to {@link ModelMemento#saveObjects}. * @param {MetaModel} metaModel The metamodel that was given to {@link ModelMemento#saveObjects}. */ restoreObjects(scene, metaModel) { const mask = this._mask; const visible = (!mask || mask.visible); const edges = (!mask || mask.edges); const xrayed = (!mask || mask.xrayed); const highlighted = (!mask || mask.highlighted); const selected = (!mask || mask.selected); const clippable = (!mask || mask.clippable); const pickable = (!mask || mask.pickable); const colorize = (!mask || mask.colorize); const opacity = (!mask || mask.opacity); const metaObjects = metaModel.metaObjects; const objects = scene.objects; for (let i = 0, len = metaObjects.length; i < len; i++) { const metaObject = metaObjects[i]; const objectId = metaObject.id; const object = objects[objectId]; if (!object) { continue; } if (visible) { object.visible = this.objectsVisible[i]; } if (edges) { object.edges = this.objectsEdges[i]; } if (xrayed) { object.xrayed = this.objectsXrayed[i]; } if (highlighted) { object.highlighted = this.objectsHighlighted[i]; } if (selected) { object.selected = this.objectsSelected[i]; } if (clippable) { object.clippable = this.objectsClippable[i]; } if (pickable) { object.pickable = this.objectsPickable[i]; } if (colorize) { color$3[0] = this.objectsColorize[i * 3 + 0]; color$3[1] = this.objectsColorize[i * 3 + 1]; color$3[2] = this.objectsColorize[i * 3 + 2]; object.colorize = color$3; } if (opacity) { object.opacity = this.objectsOpacity[i]; } } } } const color$2 = math.vec3(); /** * @desc Saves and restores a snapshot of the visual state of the {@link Entity}'s that represent objects within a {@link Scene}. * * * An Entity represents an object when {@link Entity#isObject} is ````true````. * * Each object-Entity is registered by {@link Entity#id} in {@link Scene#objects}. * * ## See Also * * * {@link CameraMemento} - Saves and restores the state of a {@link Scene}'s {@link Camera}. * * {@link ModelMemento} - Saves and restores a snapshot of the visual state of the {@link Entity}'s of a model within a {@link Scene}. * * ## Usage * * In the example below, we'll create a {@link Viewer} and use an {@link XKTLoaderPlugin} to load an ````.xkt```` model. When the model has loaded, we'll hide a couple of {@link Entity}s and save a snapshot of the visual states of all the Entitys in an ObjectsMemento. Then we'll show all the Entitys * again, and then we'll restore the visual states of all the Entitys again from the ObjectsMemento, which will hide those two Entitys again. * * ````javascript * import {Viewer, XKTLoaderPlugin, ObjectsMemento} from "xeokit-sdk.es.js"; * * const viewer = new Viewer({ * canvasId: "myCanvas" * }); * * // Load a model * const xktLoader = new XKTLoaderPlugin(viewer); * * const model = xktLoader.load({ * id: "myModel", * src: "./models/xkt/schependomlaan/schependomlaan.xkt" * }); * * model.on("loaded", () => { * * // Model has loaded * * // Hide a couple of objects * viewer.scene.objects["0u4wgLe6n0ABVaiXyikbkA"].visible = false; * viewer.scene.objects["3u4wgLe3n0AXVaiXyikbYO"].visible = false; * * // Save memento of all object states, which includes those two hidden objects * const objectsMemento = new ObjectsMemento(); * * objectsMemento.saveObjects(viewer.scene); * * // Show all objects * viewer.scene.setObjectsVisible(viewer.scene.objectIds, true); * * // Restore the objects states again, which involves hiding those two objects again * objectsMemento.restoreObjects(viewer.scene); * }); * ````` * * ## Masking Saved State * * We can optionally supply a mask to focus what state we save and restore. * * For example, to save and restore only the {@link Entity#visible} and {@link Entity#clippable} states: * * ````javascript * objectsMemento.saveObjects(viewer.scene, { * visible: true, * clippable: true * }); * * //... * * // Restore the objects states again * objectsMemento.restoreObjects(viewer.scene); * ```` */ class ObjectsMemento { /** * Creates an ObjectsMemento. */ constructor() { /** @private */ this.objectsVisible = []; /** @private */ this.objectsEdges = []; /** @private */ this.objectsXrayed = []; /** @private */ this.objectsHighlighted = []; /** @private */ this.objectsSelected = []; /** @private */ this.objectsClippable = []; /** @private */ this.objectsPickable = []; /** @private */ this.objectsColorize = []; /** @private */ this.objectsHasColorize = []; /** @private */ this.objectsOpacity = []; /** @private */ this.numObjects = 0; } /** * Saves a snapshot of the visual state of the {@link Entity}'s that represent objects within a {@link Scene}. * * @param {Scene} scene The scene. * @param {Object} [mask] Masks what state gets saved. Saves all state when not supplied. * @param {boolean} [mask.visible] Saves {@link Entity#visible} values when ````true````. * @param {boolean} [mask.visible] Saves {@link Entity#visible} values when ````true````. * @param {boolean} [mask.edges] Saves {@link Entity#edges} values when ````true````. * @param {boolean} [mask.xrayed] Saves {@link Entity#xrayed} values when ````true````. * @param {boolean} [mask.highlighted] Saves {@link Entity#highlighted} values when ````true````. * @param {boolean} [mask.selected] Saves {@link Entity#selected} values when ````true````. * @param {boolean} [mask.clippable] Saves {@link Entity#clippable} values when ````true````. * @param {boolean} [mask.pickable] Saves {@link Entity#pickable} values when ````true````. * @param {boolean} [mask.colorize] Saves {@link Entity#colorize} values when ````true````. * @param {boolean} [mask.opacity] Saves {@link Entity#opacity} values when ````true````. */ saveObjects(scene, mask) { this.numObjects = 0; this._mask = mask ? utils.apply(mask, {}) : null; const objects = scene.objects; const visible = (!mask || mask.visible); const edges = (!mask || mask.edges); const xrayed = (!mask || mask.xrayed); const highlighted = (!mask || mask.highlighted); const selected = (!mask || mask.selected); const clippable = (!mask || mask.clippable); const pickable = (!mask || mask.pickable); const colorize = (!mask || mask.colorize); const opacity = (!mask || mask.opacity); for (let objectId in objects) { if (objects.hasOwnProperty(objectId)) { const object = objects[objectId]; const i = this.numObjects; if (visible) { this.objectsVisible[i] = object.visible; } if (edges) { this.objectsEdges[i] = object.edges; } if (xrayed) { this.objectsXrayed[i] = object.xrayed; } if (highlighted) { this.objectsHighlighted[i] = object.highlighted; } if (selected) { this.objectsSelected[i] = object.selected; } if (clippable) { this.objectsClippable[i] = object.clippable; } if (pickable) { this.objectsPickable[i] = object.pickable; } if (colorize) { const objectColor = object.colorize; if (objectColor) { this.objectsColorize[i * 3 + 0] = objectColor[0]; this.objectsColorize[i * 3 + 1] = objectColor[1]; this.objectsColorize[i * 3 + 2] = objectColor[2]; this.objectsHasColorize[i] = true; } else { this.objectsHasColorize[i] = false; } } if (opacity) { this.objectsOpacity[i] = object.opacity; } this.numObjects++; } } } /** * Restores a {@link Scene}'s {@link Entity}'s to their state previously captured with {@link ObjectsMemento#saveObjects}. * @param {Scene} scene The scene. */ restoreObjects(scene) { const mask = this._mask; const visible = (!mask || mask.visible); const edges = (!mask || mask.edges); const xrayed = (!mask || mask.xrayed); const highlighted = (!mask || mask.highlighted); const selected = (!mask || mask.selected); const clippable = (!mask || mask.clippable); const pickable = (!mask || mask.pickable); const colorize = (!mask || mask.colorize); const opacity = (!mask || mask.opacity); var i = 0; const objects = scene.objects; for (let objectId in objects) { if (objects.hasOwnProperty(objectId)) { const object = objects[objectId]; if (visible) { object.visible = this.objectsVisible[i]; } if (edges) { object.edges = this.objectsEdges[i]; } if (xrayed) { object.xrayed = this.objectsXrayed[i]; } if (highlighted) { object.highlighted = this.objectsHighlighted[i]; } if (selected) { object.selected = this.objectsSelected[i]; } if (clippable) { object.clippable = this.objectsClippable[i]; } if (pickable) { object.pickable = this.objectsPickable[i]; } if (colorize ) { if (this.objectsHasColorize[i]) { color$2[0] = this.objectsColorize[i * 3 + 0]; color$2[1] = this.objectsColorize[i * 3 + 1]; color$2[2] = this.objectsColorize[i * 3 + 2]; object.colorize = color$2; } else { object.colorize = null; } } if (opacity) { object.opacity = this.objectsOpacity[i]; } i++; } } } } /** * @desc A {@link Curve} along which a 3D position can be animated. * * * As shown in the diagram below, a CubicBezierCurve is defined by four control points. * * You can sample a {@link CubicBezierCurve#point} and a {@link CubicBezierCurve#tangent} vector on a CubicBezierCurve for any given value of {@link CubicBezierCurve#t} in the range [0..1]. * * When you set {@link CubicBezierCurve#t} on a CubicBezierCurve, its {@link CubicBezierCurve#point} and {@link CubicBezierCurve#tangent} properties will update accordingly. * * To build a complex path, you can combine an unlimited combination of CubicBezierCurves, {@link QuadraticBezierCurve}s and {@link SplineCurve}s into a {@link Path}. * *
* *
* [Cubic Bezier Curve from WikiPedia](https://en.wikipedia.org/wiki/B%C3%A9zier_curve) */ class CubicBezierCurve extends Curve { /** * @constructor * @param {Component} [owner] Owner component. When destroyed, the owner will destroy this CubicBezierCurve as well. * @param {*} [cfg] Configs * @param {String} [cfg.id] Optional ID, unique among all components in the parent {@link Scene}, generated automatically when omitted. * @param {Number[]} [cfg.v0=[0,0,0]] The starting point. * @param {Number[]} [cfg.v1=[0,0,0]] The first control point. * @param {Number[]} [cfg.v2=[0,0,0]] The middle control point. * @param {Number[]} [cfg.v3=[0,0,0]] The ending point. * @param {Number} [cfg.t=0] Current position on this CubicBezierCurve, in range between 0..1. */ constructor(owner, cfg = {}) { super(owner, cfg); this.v0 = cfg.v0; this.v1 = cfg.v1; this.v2 = cfg.v2; this.v3 = cfg.v3; this.t = cfg.t; } /** * Sets the starting point on this CubicBezierCurve. * * Default value is ````[0.0, 0.0, 0.0]```` * * @param {Number[]} value The starting point. */ set v0(value) { this._v0 = value || math.vec3([0, 0, 0]); } /** * Gets the starting point on this CubicBezierCurve. * * Default value is ````[0.0, 0.0, 0.0]```` * * @returns {Number[]} The starting point. */ get v0() { return this._v0; } /** * Sets the first control point on this CubicBezierCurve. * * Default value is ````[0.0, 0.0, 0.0]```` * * @param {Number[]} value The first control point. */ set v1(value) { this._v1 = value || math.vec3([0, 0, 0]); } /** * Gets the first control point on this CubicBezierCurve. * * Fires a {@link CubicBezierCurve#v1:event} event on change. * * Default value is ````[0.0, 0.0, 0.0]```` * * @returns {Number[]} The first control point. */ get v1() { return this._v1; } /** * Sets the second control point on this CubicBezierCurve. * * Default value is ````[0.0, 0.0, 0.0]```` * * @param {Number[]} value The second control point. */ set v2(value) { this._v2 = value || math.vec3([0, 0, 0]); } /** * Gets the second control point on this CubicBezierCurve. * * Default value is ````[0.0, 0.0, 0.0]```` * * @returns {Number[]} The second control point. */ get v2() { return this._v2; } /** * Sets the end point on this CubicBezierCurve. * * Fires a {@link CubicBezierCurve#v3:event} event on change. * * Default value is ````[0.0, 0.0, 0.0]```` * * @param {Number[]} value The end point. */ set v3(value) { this.fire("v3", this._v3 = value || math.vec3([0, 0, 0])); } /** * Gets the end point on this CubicBezierCurve. * * Fires a {@link CubicBezierCurve#v3:event} event on change. * * Default value is ````[0.0, 0.0, 0.0]```` * * @returns {Number[]} The end point. */ get v3() { return this._v3; } /** * Sets the current position of progress along this CubicBezierCurve. * * Automatically clamps to range ````[0..1]````. * * @param {Number} value New progress time value. */ set t(value) { value = value || 0; this._t = value < 0.0 ? 0.0 : (value > 1.0 ? 1.0 : value); } /** * Gets the current position of progress along this CubicBezierCurve. * * @returns {Number} Current progress time value. */ get t() { return this._t; } /** * Returns point on this CubicBezierCurve at the given position. * * @param {Number} t Position to get point at. * * @returns {Number[]} The point at the given position. */ get point() { return this.getPoint(this._t); } /** * Returns point on this CubicBezierCurve at the given position. * * @param {Number} t Position to get point at. * * @returns {Number[]} The point at the given position. */ getPoint(t) { var vector = math.vec3(); vector[0] = math.b3(t, this._v0[0], this._v1[0], this._v2[0], this._v3[0]); vector[1] = math.b3(t, this._v0[1], this._v1[1], this._v2[1], this._v3[1]); vector[2] = math.b3(t, this._v0[2], this._v1[2], this._v2[2], this._v3[2]); return vector; } getJSON() { return { v0: this._v0, v1: this._v1, v2: this._v2, v3: this._v3, t: this._t }; } } /** * @desc A complex curved path constructed from various {@link Curve} subtypes. * * * A Path can be constructed from these {@link Curve} subtypes: {@link SplineCurve}, {@link CubicBezierCurve} and {@link QuadraticBezierCurve}. * * You can sample a {@link Path#point} and a {@link Curve#tangent} vector on a Path for any given value of {@link Path#t} in the range ````[0..1]````. * * When you set {@link Path#t} on a Path, its {@link Path#point} and {@link Curve#tangent} properties will update accordingly. */ class Path extends Curve { /** * @constructor * @param {Component} [owner] Owner component. When destroyed, the owner will destroy this SectionPlane as well. * @param {*} [cfg] Path configuration * @param {String} [cfg.id] Optional ID, unique among all components in the parent {@link Scene}, generated automatically when omitted. * @param {String []} [cfg.paths=[]] IDs or instances of {{#crossLink "path"}}{{/crossLink}} subtypes to add to this Path. * @param {Number} [cfg.t=0] Current position on this Path, in range between 0..1. */ constructor(owner, cfg = {}) { super(owner, cfg); this._cachedLengths = []; this._dirty = true; this._curves = []; // Array of child Curve components this._t = 0; this._dirtySubs = []; // Subscriptions to "dirty" events from child Curve components this._destroyedSubs = []; // Subscriptions to "destroyed" events from child Curve components this.curves = cfg.curves || []; // Add initial curves this.t = cfg.t; // Set initial progress } /** * Adds a {@link Curve} to this Path. * * @param {Curve} curve The {@link Curve} to add. */ addCurve(curve) { this._curves.push(curve); this._dirty = true; } /** * Sets the {@link Curve}s in this Path. * * Default value is ````[]````. * * @param {{Array of Spline, Path, QuadraticBezierCurve or CubicBezierCurve}} value. */ set curves(value) { value = value || []; var curve; // Unsubscribe from events on old curves var i; var len; for (i = 0, len = this._curves.length; i < len; i++) { curve = this._curves[i]; curve.off(this._dirtySubs[i]); curve.off(this._destroyedSubs[i]); } this._curves = []; this._dirtySubs = []; this._destroyedSubs = []; var self = this; function curveDirty() { self._dirty = true; } function curveDestroyed() { var id = this.id; for (i = 0, len = self._curves.length; i < len; i++) { if (self._curves[i].id === id) { self._curves = self._curves.slice(i, i + 1); self._dirtySubs = self._dirtySubs.slice(i, i + 1); self._destroyedSubs = self._destroyedSubs.slice(i, i + 1); self._dirty = true; return; } } } for (i = 0, len = value.length; i < len; i++) { curve = value[i]; if (utils.isNumeric(curve) || utils.isString(curve)) { // ID given for curve - find the curve component var id = curve; curve = this.scene.components[id]; if (!curve) { this.error("Component not found: " + _inQuotes(id)); continue; } } var type = curve.type; if (type !== "xeokit.SplineCurve" && type !== "xeokit.Path" && type !== "xeokit.CubicBezierCurve" && type !== "xeokit.QuadraticBezierCurve") { this.error("Component " + _inQuotes(curve.id) + " is not a xeokit.SplineCurve, xeokit.Path or xeokit.QuadraticBezierCurve"); continue; } this._curves.push(curve); this._dirtySubs.push(curve.on("dirty", curveDirty)); this._destroyedSubs.push(curve.once("destroyed", curveDestroyed)); } this._dirty = true; } /** * Gets the {@link Curve}s in this Path. * * @returns {{Array of Spline, Path, QuadraticBezierCurve or CubicBezierCurve}} the {@link Curve}s in this path. */ get curves() { return this._curves; } /** * Sets the current point of progress along this Path. * * Automatically clamps to range ````[0..1]````. * * Default value is ````0````. * * @param {Number} value The current point of progress. */ set t(value) { value = value || 0; this._t = value < 0.0 ? 0.0 : (value > 1.0 ? 1.0 : value); } /** * Gets the current point of progress along this Path. * * Default value is ````0````. * * @returns {Number} The current point of progress. */ get t() { return this._t; } /** * Gets point on this Path corresponding to the current value of {@link Path#t}. * * @returns {Number[]} The point. */ get point() { return this.getPoint(this._t); } /** * Length of this Path, which is the cumulative length of all {@link Curve}s currently in {@link Path#curves}. * * @return {Number} Length of this path. */ get length() { var lens = this._getCurveLengths(); return lens[lens.length - 1]; } /** * Gets a point on this Path corresponding to the given progress position. * * @param {Number} t Indicates point of progress along this curve, in the range [0..1]. * @returns {Number[]} */ getPoint(t) { var d = t * this.length; var curveLengths = this._getCurveLengths(); var i = 0, diff, curve; while (i < curveLengths.length) { if (curveLengths[i] >= d) { diff = curveLengths[i] - d; curve = this._curves[i]; var u = 1 - diff / curve.length; return curve.getPointAt(u); } i++; } return null; } _getCurveLengths() { if (!this._dirty) { return this._cachedLengths; } var lengths = []; var sums = 0; var i, il = this._curves.length; for (i = 0; i < il; i++) { sums += this._curves[i].length; lengths.push(sums); } this._cachedLengths = lengths; this._dirty = false; return lengths; } _getJSON() { var curveIds = []; for (var i = 0, len = this._curves.length; i < len; i++) { curveIds.push(this._curves[i].id); } return { curves: curveIds, t: this._t }; } /** * Destroys this Path. */ destroy() { super.destroy(); var i; var len; var curve; for (i = 0, len = this._curves.length; i < len; i++) { curve = this._curves[i]; curve.off(this._dirtySubs[i]); curve.off(this._destroyedSubs[i]); } } } /** * A **QuadraticBezierCurve** is a {@link Curve} along which a 3D position can be animated. * * * As shown in the diagram below, a QuadraticBezierCurve is defined by three control points * * You can sample a {@link QuadraticBezierCurve#point} and a {@link Curve#tangent} vector on a QuadraticBezierCurve for any given value of {@link QuadraticBezierCurve#t} in the range ````[0..1]```` * * When you set {@link QuadraticBezierCurve#t} on a QuadraticBezierCurve, its {@link QuadraticBezierCurve#point} and {@link Curve#tangent} will update accordingly. * * To build a complex path, you can combine an unlimited combination of QuadraticBezierCurves, {@link CubicBezierCurve}s and {@link SplineCurve}s into a {@link Path}. *
* *
* *[Quadratic Bezier Curve from WikiPedia](https://en.wikipedia.org/wiki/B%C3%A9zier_curve)* */ class QuadraticBezierCurve extends Curve { /** * @constructor * @param {Component} owner Owner component. When destroyed, the owner will destroy this MetallicMaterial as well. * @param {*} [cfg] Configuration * @param {String} [cfg.id] Optional ID, unique among all components in the parent {{#crossLink "Scene"}}Scene{{/crossLink}}, generated automatically when omitted. * @param {Number[]} [cfg.v0=[0,0,0]] The starting point. * @param {Number[]} [cfg.v1=[0,0,0]] The middle control point. * @param {Number[]} [cfg.v2=[0,0,0]] The end point. * @param {Number[]} [cfg.t=0] Current position on this QuadraticBezierCurve, in range between ````0..1````. */ constructor(owner, cfg = {}) { super(owner, cfg); this.v0 = cfg.v0; this.v1 = cfg.v1; this.v2 = cfg.v2; this.t = cfg.t; } /** * Sets the starting point on this QuadraticBezierCurve. * * Default value is ````[0.0, 0.0, 0.0]````. * * @param {Number[]} value New starting point. */ set v0(value) { this._v0 = value || math.vec3([0, 0, 0]); } /** * Gets the starting point on this QuadraticBezierCurve. * * Default value is ````[0.0, 0.0, 0.0]````. * * @returns {Number[]} The starting point. */ get v0() { return this._v0; } /** * Sets the middle control point on this QuadraticBezierCurve. * * Default value is ````[0.0, 0.0, 0.0]````. * * @param {Number[]} value New middle control point. */ set v1(value) { this._v1 = value || math.vec3([0, 0, 0]); } /** * Gets the middle control point on this QuadraticBezierCurve. * * Default value is ````[0.0, 0.0, 0.0]````. * * @returns {Number[]} The middle control point. */ get v1() { return this._v1; } /** * Sets the end point on this QuadraticBezierCurve. * * Default value is ````[0.0, 0.0, 0.0]````. * * @param {Number[]} value The new end point. */ set v2(value) { this._v2 = value || math.vec3([0, 0, 0]); } /** * Gets the end point on this QuadraticBezierCurve. * * Default value is ````[0.0, 0.0, 0.0]````. * * @returns {Number[]} The end point. */ get v2() { return this._v2; } /** * Sets the progress along this QuadraticBezierCurve. * * Automatically clamps to range [0..1]. * * Default value is ````0````. * * @param {Number} value The new progress location. */ set t(value) { value = value || 0; this._t = value < 0.0 ? 0.0 : (value > 1.0 ? 1.0 : value); } /** * Gets the progress along this QuadraticBezierCurve. * * Default value is ````0````. * * @returns {Number} The current progress location. */ get t() { return this._t; } /** Point on this QuadraticBezierCurve at position {@link QuadraticBezierCurve/t}. @property point @type {Number[]} */ get point() { return this.getPoint(this._t); } /** * Returns the point on this QuadraticBezierCurve at the given position. * * @param {Number} t Position to get point at. * @returns {Number[]} The point. */ getPoint(t) { var vector = math.vec3(); vector[0] = math.b2(t, this._v0[0], this._v1[0], this._v2[0]); vector[1] = math.b2(t, this._v0[1], this._v1[1], this._v2[1]); vector[2] = math.b2(t, this._v0[2], this._v1[2], this._v2[2]); return vector; } getJSON() { return { v0: this._v0, v1: this._v1, v2: this._v2, t: this._t }; } } /** * @desc A high-performance model representation for efficient rendering and low memory usage. * * * PerformanceModel was replaced with {@link SceneModel} in ````xeokit-sdk v2.4````. * * PerformanceModel currently extends {@link SceneModel}, in order to maintain backward-compatibility until we remove PerformanceModel. * * See {@link SceneModel} for API details. * * @deprecated * @implements {Drawable} * @implements {Entity} * @extends {SceneModel} */ class PerformanceModel extends SceneModel { /** * See {@link VBOSceneModel} for details. * * @param owner * @param cfg */ constructor(owner, cfg = {}) { super(owner, cfg); } } const tempVec3a$c = math.vec3(); const tempVec3b$9 = math.vec3(); const tempVec4a$8 = math.vec4(); const front = math.vec3([0, 0, -1]); const back = math.vec3([0, 0, 1]); const up = math.vec3([0, 1, 0]); /** * @desc An arbitrarily-aligned World-space clipping plane. * * * Slices portions off objects to create cross-section views or reveal interiors. * * Registered by {@link SectionPlane#id} in {@link Scene#sectionPlanes}. * * Indicates World-space position in {@link SectionPlane#pos} and orientation in {@link SectionPlane#dir}. * * Discards elements from the half-space in the direction of {@link SectionPlane#dir}. * * Can be be enabled or disabled via {@link SectionPlane#active}. * * ## Usage * * In the example below, we'll create two SectionPlanes to slice a model loaded from glTF. Note that we could also create them * using a {@link SectionPlanesPlugin}. * * ````javascript * import {Viewer, GLTFLoaderPlugin, SectionPlane} from "xeokit-sdk.es.js"; * * const viewer = new Viewer({ * canvasId: "myCanvas" * }); * * const gltfLoaderPlugin = new GLTFModelsPlugin(viewer, { * id: "GLTFModels" * }); * * const model = gltfLoaderPlugin.load({ * id: "myModel", * src: "./models/gltf/mygltfmodel.gltf" * }); * * // Create a SectionPlane on negative diagonal * const sectionPlane1 = new SectionPlane(viewer.scene, { * pos: [1.0, 1.0, 1.0], * dir: [-1.0, -1.0, -1.0], * active: true * }), * * // Create a SectionPlane on positive diagonal * const sectionPlane2 = new SectionPlane(viewer.scene, { * pos: [-1.0, -1.0, -1.0], * dir: [1.0, 1.0, 1.0], * active: true * }); * ```` */ class SectionPlane extends Component { /** @private */ get type() { return "SectionPlane"; } /** * @constructor * @param {Component} [owner] Owner component. When destroyed, the owner will destroy this SectionPlane as well. * @param {*} [cfg] SectionPlane configuration * @param {String} [cfg.id] Optional ID, unique among all components in the parent {@link Scene}, generated automatically when omitted. * @param {Boolean} [cfg.active=true] Indicates whether or not this SectionPlane is active. * @param {Number[]} [cfg.pos=[0,0,0]] World-space position of the SectionPlane. * @param {Number[]} [cfg.dir=[0,0,-1]] Vector perpendicular to the plane surface, indicating the SectionPlane plane orientation. */ constructor(owner, cfg = {}) { super(owner, cfg); this._state = new RenderState({ active: true, pos: math.vec3(), quaternion: math.vec4(), roll: 0, dir: math.vec3(), dist: 0 }); this.active = cfg.active; this.pos = cfg.pos; this.dir = cfg.dir; this.scene._sectionPlaneCreated(this); } /** * Sets if this SectionPlane is active or not. * * Default value is ````true````. * * @param {Boolean} value Set ````true```` to activate else ````false```` to deactivate. */ set active(value) { this._state.active = value !== false; this.glRedraw(); this.fire("active", this._state.active); this.scene.fire("sectionPlaneUpdated", this); } /** * Gets if this SectionPlane is active or not. * * Default value is ````true````. * * @returns {Boolean} Returns ````true```` if active. */ get active() { return this._state.active; } /** * Sets the World-space position of this SectionPlane's plane. * * Default value is ````[0, 0, 0]````. * * @param {Number[]} value New position. */ set pos(value) { this._state.pos.set(value || [0, 0, 0]); this._state.dist = (-math.dotVec3(this._state.pos, this._state.dir)); this.fire("pos", this._state.pos); this.scene.fire("sectionPlaneUpdated", this); } /** * Gets the World-space position of this SectionPlane's plane. * * Default value is ````[0, 0, 0]````. * * @returns {Number[]} Current position. */ get pos() { return this._state.pos; } /** * Sets the quaternion of this SectionPlane's plane. * * Default value is ````[0, -1, 0, 0]````. * * @param {Number[]} value New quaternion. */ set quaternion(value) { this._state.quaternion.set(value || [0, 0, 0, -1]); math.vec3ApplyQuaternion(this._state.quaternion, back, this._state.dir); const quatUp = math.vec3ApplyQuaternion(this._state.quaternion, up, tempVec3a$c); const dirOnlyQ = math.vec3PairToQuaternion(back, this._state.dir, tempVec4a$8); const dirOnlyUp = math.vec3ApplyQuaternion(dirOnlyQ, up, tempVec3b$9); const angle = Math.acos(Math.min(1, math.dotVec3(quatUp, dirOnlyUp))); const sign = Math.sign(math.dotVec3(this._state.dir, math.cross3Vec3(quatUp, dirOnlyUp, tempVec3b$9))); this._state.roll = sign * angle; this._onDirUpdated(); } /** * Gets the quaternion of this SectionPlane's plane. * * Default value is ````[0, -1, 0, 0]````. * * @returns {Number[]} value Current quaternion. */ get quaternion() { return this._state.quaternion; } /** * Sets the roll of this SectionPlane's plane. * * Default value is ````0````. * * @param {Number[]} value New roll. */ set roll(value) { this._state.roll = value || 0; this._onDirRollUpdated(); } /** * Gets the roll of this SectionPlane's plane. * * Default value is ````0````. * * @returns {Number[]} value Current roll. */ get roll() { return this._state.roll; } /** * Sets the direction of this SectionPlane's plane. * * Default value is ````[0, 0, -1]````. * * @param {Number[]} value New direction. */ set dir(value) { this._state.dir.set(value || front); this._onDirRollUpdated(); } _onDirRollUpdated() { math.vec3PairToQuaternion(back, this._state.dir, this._state.quaternion); tempVec4a$8[0] = 0; tempVec4a$8[1] = 0; tempVec4a$8[2] = -1; tempVec4a$8[3] = this._state.roll; math.angleAxisToQuaternion(tempVec4a$8, tempVec4a$8); math.mulQuaternions(this._state.quaternion, tempVec4a$8, this._state.quaternion); this._onDirUpdated(); } _onDirUpdated() { this._state.dist = (-math.dotVec3(this._state.pos, this._state.dir)); this.glRedraw(); this.fire("dir", this._state.dir); this.scene.fire("sectionPlaneUpdated", this); } /** * Gets the direction of this SectionPlane's plane. * * Default value is ````[0, 0, -1]````. * * @returns {Number[]} value Current direction. */ get dir() { return this._state.dir; } /** * Gets this SectionPlane's distance to the origin of the World-space coordinate system. * * This is the dot product of {@link SectionPlane#pos} and {@link SectionPlane#dir} and is automatically re-calculated * each time either of two properties are updated. * * @returns {Number} */ get dist() { return this._state.dist; } /** * Inverts the direction of {@link SectionPlane#dir}. */ flipDir() { math.mulVec3Scalar(this._state.dir, -1.0, this._state.dir); this.dir = this._state.dir; } /** * @destroy */ destroy() { this._state.destroy(); this.scene._sectionPlaneDestroyed(this); super.destroy(); } } /** * Transcodes texture data. * * A {@link SceneModel} configured with an appropriate TextureTranscoder will allow us to add textures from * transcoded buffers or files. For a concrete example, see {@link VBOSceneModel}, which can be configured with * a {@link KTX2TextureTranscoder}, which allows us to add textures from KTX2 buffers and files. * * @interface */ class TextureTranscoder { /** * Transcodes texture data from transcoded buffers into a {@link Texture2D}. * * @param {ArrayBuffer[]} buffers Transcoded texture data. Given as an array of buffers so that we can support * multi-image textures, such as cube maps. * @param {*} config Transcoding options. * @param {Texture2D} texture The texture to load. * @returns {Promise} Resolves when the texture has loaded. */ transcode(buffers, texture, config = {}) { } /** * Destroys this transcoder. */ destroy() { } } /** * @desc Pick result returned by {@link Scene#pick}. * */ class PickResult { /** * @private */ constructor() { /** * Picked entity. * Null when no entity was picked. * @property entity * @type {Entity|*} */ this.entity = null; /** * Type of primitive that was picked - usually "triangle". * Null when no primitive was picked. * @property primitive * @type {String} */ this.primitive = null; /** * Index of primitive that was picked. * -1 when no entity was picked. * @property primIndex * @type {number} */ this.primIndex = -1; /** * True when the picked surface position is full precision. * When false, the picked surface position should be regarded as approximate. * Full-precision surface picking is performed with the {@link Scene#pick} method's ````pickSurfacePrecision```` option. * @property pickSurfacePrecision * @type {Boolean} */ this.pickSurfacePrecision = false; /** * True when picked from touch input, else false when from mouse input. * @type {boolean} */ this.touchInput = false; /** * True when snapped to nearest edge. * @type {boolean} */ this.snappedToEdge = false; /** * True when snapped to nearest vertex. * @type {boolean} */ this.snappedToVertex = false; this._origin = new Float64Array([0, 0, 0]); this._direction = new Float64Array([0, 0, 0]); this._indices = new Int32Array(3); this._localPos = new Float64Array([0, 0, 0]); this._worldPos = new Float64Array([0, 0, 0]); this._viewPos = new Float64Array([0, 0, 0]); this._canvasPos = new Int16Array([0, 0]); this._snappedCanvasPos = new Int16Array([0, 0]); this._bary = new Float64Array([0, 0, 0]); this._worldNormal = new Float64Array([0, 0, 0]); this._uv = new Float64Array([0, 0]); this.reset(); } /** * Canvas pick coordinates. * @property canvasPos * @type {Number[]} */ get canvasPos() { return this._gotCanvasPos ? this._canvasPos : null; } /** * @private * @param value */ set canvasPos(value) { if (value) { this._canvasPos[0] = value[0]; this._canvasPos[1] = value[1]; this._gotCanvasPos = true; } else { this._gotCanvasPos = false; } } /** * World-space 3D ray origin when raypicked. * @property origin * @type {Number[]} */ get origin() { return this._gotOrigin ? this._origin : null; } /** * @private * @param value */ set origin(value) { if (value) { this._origin[0] = value[0]; this._origin[1] = value[1]; this._origin[2] = value[2]; this._gotOrigin = true; } else { this._gotOrigin = false; } } /** * World-space 3D ray direction when raypicked. * @property direction * @type {Number[]} */ get direction() { return this._gotDirection ? this._direction : null; } /** * @private * @param value */ set direction(value) { if (value) { this._direction[0] = value[0]; this._direction[1] = value[1]; this._direction[2] = value[2]; this._gotDirection = true; } else { this._gotDirection = false; } } /** * Picked triangle's vertex indices. * @property indices * @type {Int32Array} */ get indices() { return this.entity && this._gotIndices ? this._indices : null; } /** * @private * @param value */ set indices(value) { if (value) { this._indices[0] = value[0]; this._indices[1] = value[1]; this._indices[2] = value[2]; this._gotIndices = true; } else { this._gotIndices = false; } } /** * Picked Local-space point. * @property localPos * @type {Number[]} */ get localPos() { return this.entity && this._gotLocalPos ? this._localPos : null; } /** * @private * @param value */ set localPos(value) { if (value) { this._localPos[0] = value[0]; this._localPos[1] = value[1]; this._localPos[2] = value[2]; this._gotLocalPos = true; } else { this._gotLocalPos = false; } } /** * Canvas cursor coordinates, snapped when snap picking, otherwise same as {@link PickResult#pointerPos}. * @property snappedCanvasPos * @type {Number[]} */ get snappedCanvasPos() { return this._gotSnappedCanvasPos ? this._snappedCanvasPos : null; } /** * @private * @param value */ set snappedCanvasPos(value) { if (value) { this._snappedCanvasPos[0] = value[0]; this._snappedCanvasPos[1] = value[1]; this._gotSnappedCanvasPos = true; } else { this._gotSnappedCanvasPos = false; } } /** * Picked World-space point. * @property worldPos * @type {Number[]} */ get worldPos() { return this._gotWorldPos ? this._worldPos : null; } /** * @private * @param value */ set worldPos(value) { if (value) { this._worldPos[0] = value[0]; this._worldPos[1] = value[1]; this._worldPos[2] = value[2]; this._gotWorldPos = true; } else { this._gotWorldPos = false; } } /** * Picked View-space point. * @property viewPos * @type {Number[]} */ get viewPos() { return this.entity && this._gotViewPos ? this._viewPos : null; } /** * @private * @param value */ set viewPos(value) { if (value) { this._viewPos[0] = value[0]; this._viewPos[1] = value[1]; this._viewPos[2] = value[2]; this._gotViewPos = true; } else { this._gotViewPos = false; } } /** * Barycentric coordinate within picked triangle. * @property bary * @type {Number[]} */ get bary() { return this.entity && this._gotBary ? this._bary : null; } /** * @private * @param value */ set bary(value) { if (value) { this._bary[0] = value[0]; this._bary[1] = value[1]; this._bary[2] = value[2]; this._gotBary = true; } else { this._gotBary = false; } } /** * Normal vector at picked position on surface. * @property worldNormal * @type {Number[]} */ get worldNormal() { return this.entity && this._gotWorldNormal ? this._worldNormal : null; } /** * @private * @param value */ set worldNormal(value) { if (value) { this._worldNormal[0] = value[0]; this._worldNormal[1] = value[1]; this._worldNormal[2] = value[2]; this._gotWorldNormal = true; } else { this._gotWorldNormal = false; } } /** * UV coordinates at picked position on surface. * @property uv * @type {Number[]} */ get uv() { return this.entity && this._gotUV ? this._uv : null; } /** * @private * @param value */ set uv(value) { if (value) { this._uv[0] = value[0]; this._uv[1] = value[1]; this._gotUV = true; } else { this._gotUV = false; } } /** * True if snapped to edge or vertex. * @returns {boolean} */ get snapped() { return this.snappedToEdge || this.snappedToVertex; } /** * @private */ reset() { this.entity = null; this.primIndex = -1; this.primitive = null; this.pickSurfacePrecision = false; this._gotCanvasPos = false; this._gotSnappedCanvasPos = false; this._gotOrigin = false; this._gotDirection = false; this._gotIndices = false; this._gotLocalPos = false; this._gotWorldPos = false; this._gotViewPos = false; this._gotBary = false; this._gotWorldNormal = false; this._gotUV = false; this.touchInput = false; this.snappedToEdge = false; this.snappedToVertex = false; } } const os = { isIphoneSafari() { const userAgent = window.navigator.userAgent; const isIphone = /iPhone/i.test(userAgent); const isSafari = /Safari/i.test(userAgent) && !/Chrome/i.test(userAgent); return isIphone && isSafari; }, isTouchDevice() { return ( 'ontouchstart' in window || //works for most devices navigator.maxTouchPoints > 0 || //works for modern touch devices navigator.mxMaxTouchPoints > 0 //works for older microsoft touch devices ) } }; /** @desc Base class for {@link Viewer} plugin classes. */ class Plugin { /** * Creates this Plugin and installs it into the given {@link Viewer}. * * @param {string} id ID for this plugin, unique among all plugins in the viewer. * @param {Viewer} viewer The viewer. * @param {Object} [cfg] Options */ constructor(id, viewer, cfg) { /** * ID for this Plugin, unique within its {@link Viewer}. * * @type {string} */ this.id = (cfg && cfg.id) ? cfg.id : id; /** * The Viewer that contains this Plugin. * * @type {Viewer} */ this.viewer = viewer; this._subIdMap = null; // Subscription subId pool this._subIdEvents = null; // Subscription subIds mapped to event names this._eventSubs = null; // Event names mapped to subscribers this._eventSubsNum = null; this._events = null; // Maps names to events this._eventCallDepth = 0; // Helps us catch stack overflows from recursive events viewer.addPlugin(this); } /** * Schedule a task to perform on the next browser interval * @param task */ scheduleTask(task) { core.scheduleTask(task, null); } /** * Fires an event on this Plugin. * * Notifies existing subscribers to the event, optionally retains the event to give to * any subsequent notifications on the event as they are made. * * @param {String} event The event type name * @param {Object} value The event parameters * @param {Boolean} [forget=false] When true, does not retain for subsequent subscribers */ fire(event, value, forget) { if (!this._events) { this._events = {}; } if (!this._eventSubs) { this._eventSubs = {}; this._eventSubsNum = {}; } if (forget !== true) { this._events[event] = value || true; // Save notification } const subs = this._eventSubs[event]; let sub; if (subs) { // Notify subscriptions for (const subId in subs) { if (subs.hasOwnProperty(subId)) { sub = subs[subId]; this._eventCallDepth++; if (this._eventCallDepth < 300) { sub.callback.call(sub.scope, value); } else { this.error("fire: potential stack overflow from recursive event '" + event + "' - dropping this event"); } this._eventCallDepth--; } } } } /** * Subscribes to an event on this Plugin. * * The callback is be called with this Plugin as scope. * * @param {String} event The event * @param {Function} callback Called fired on the event * @param {Object} [scope=this] Scope for the callback * @return {String} Handle to the subscription, which may be used to unsubscribe with {@link #off}. */ on(event, callback, scope) { if (!this._events) { this._events = {}; } if (!this._subIdMap) { this._subIdMap = new Map$1(); // Subscription subId pool } if (!this._subIdEvents) { this._subIdEvents = {}; } if (!this._eventSubs) { this._eventSubs = {}; } if (!this._eventSubsNum) { this._eventSubsNum = {}; } let subs = this._eventSubs[event]; if (!subs) { subs = {}; this._eventSubs[event] = subs; this._eventSubsNum[event] = 1; } else { this._eventSubsNum[event]++; } const subId = this._subIdMap.addItem(); // Create unique subId subs[subId] = { callback: callback, scope: scope || this }; this._subIdEvents[subId] = event; const value = this._events[event]; if (value !== undefined) { // A publication exists, notify callback immediately callback.call(scope || this, value); } return subId; } /** * Cancels an event subscription that was previously made with {@link Plugin#on} or {@link Plugin#once}. * * @param {String} subId Subscription ID */ off(subId) { if (subId === undefined || subId === null) { return; } if (!this._subIdEvents) { return; } const event = this._subIdEvents[subId]; if (event) { delete this._subIdEvents[subId]; const subs = this._eventSubs[event]; if (subs) { delete subs[subId]; this._eventSubsNum[event]--; } this._subIdMap.removeItem(subId); // Release subId } } /** * Subscribes to the next occurrence of the given event, then un-subscribes as soon as the event is subIdd. * * This is equivalent to calling {@link Plugin#on}, and then calling {@link Plugin#off} inside the callback function. * * @param {String} event Data event to listen to * @param {Function} callback Called when fresh data is available at the event * @param {Object} [scope=this] Scope for the callback */ once(event, callback, scope) { const self = this; const subId = this.on(event, function (value) { self.off(subId); callback.call(scope || this, value); }, scope); } /** * Returns true if there are any subscribers to the given event on this Plugin. * * @param {String} event The event * @return {Boolean} True if there are any subscribers to the given event on this Plugin. */ hasSubs(event) { return (this._eventSubsNum && (this._eventSubsNum[event] > 0)); } /** * Logs a message to the JavaScript developer console, prefixed with the ID of this Plugin. * * @param {String} msg The error message */ log(msg) { console.log(`[xeokit plugin ${this.id}]: ${msg}`); } /** * Logs a warning message to the JavaScript developer console, prefixed with the ID of this Plugin. * * @param {String} msg The error message */ warn(msg) { console.warn(`[xeokit plugin ${this.id}]: ${msg}`); } /** * Logs an error message to the JavaScript developer console, prefixed with the ID of this Plugin. * * @param {String} msg The error message */ error(msg) { console.error(`[xeokit plugin ${this.id}]: ${msg}`); } /** * Sends a message to this Plugin. * * @private */ send(name, value) { //... } /** * Destroys this Plugin and removes it from its {@link Viewer}. */ destroy() { this.viewer.removePlugin(this); } } const defaultCSS = ".sk-fading-circle {\ background: transparent;\ margin: 20px auto;\ width: 50px;\ height:50px;\ position: relative;\ }\ .sk-fading-circle .sk-circle {\ width: 120%;\ height: 120%;\ position: absolute;\ left: 0;\ top: 0;\ }\ .sk-fading-circle .sk-circle:before {\ content: '';\ display: block;\ margin: 0 auto;\ width: 15%;\ height: 15%;\ background-color: #ff8800;\ border-radius: 100%;\ -webkit-animation: sk-circleFadeDelay 1.2s infinite ease-in-out both;\ animation: sk-circleFadeDelay 1.2s infinite ease-in-out both;\ }\ .sk-fading-circle .sk-circle2 {\ -webkit-transform: rotate(30deg);\ -ms-transform: rotate(30deg);\ transform: rotate(30deg);\ }\ .sk-fading-circle .sk-circle3 {\ -webkit-transform: rotate(60deg);\ -ms-transform: rotate(60deg);\ transform: rotate(60deg);\ }\ .sk-fading-circle .sk-circle4 {\ -webkit-transform: rotate(90deg);\ -ms-transform: rotate(90deg);\ transform: rotate(90deg);\ }\ .sk-fading-circle .sk-circle5 {\ -webkit-transform: rotate(120deg);\ -ms-transform: rotate(120deg);\ transform: rotate(120deg);\ }\ .sk-fading-circle .sk-circle6 {\ -webkit-transform: rotate(150deg);\ -ms-transform: rotate(150deg);\ transform: rotate(150deg);\ }\ .sk-fading-circle .sk-circle7 {\ -webkit-transform: rotate(180deg);\ -ms-transform: rotate(180deg);\ transform: rotate(180deg);\ }\ .sk-fading-circle .sk-circle8 {\ -webkit-transform: rotate(210deg);\ -ms-transform: rotate(210deg);\ transform: rotate(210deg);\ }\ .sk-fading-circle .sk-circle9 {\ -webkit-transform: rotate(240deg);\ -ms-transform: rotate(240deg);\ transform: rotate(240deg);\ }\ .sk-fading-circle .sk-circle10 {\ -webkit-transform: rotate(270deg);\ -ms-transform: rotate(270deg);\ transform: rotate(270deg);\ }\ .sk-fading-circle .sk-circle11 {\ -webkit-transform: rotate(300deg);\ -ms-transform: rotate(300deg);\ transform: rotate(300deg);\ }\ .sk-fading-circle .sk-circle12 {\ -webkit-transform: rotate(330deg);\ -ms-transform: rotate(330deg);\ transform: rotate(330deg);\ }\ .sk-fading-circle .sk-circle2:before {\ -webkit-animation-delay: -1.1s;\ animation-delay: -1.1s;\ }\ .sk-fading-circle .sk-circle3:before {\ -webkit-animation-delay: -1s;\ animation-delay: -1s;\ }\ .sk-fading-circle .sk-circle4:before {\ -webkit-animation-delay: -0.9s;\ animation-delay: -0.9s;\ }\ .sk-fading-circle .sk-circle5:before {\ -webkit-animation-delay: -0.8s;\ animation-delay: -0.8s;\ }\ .sk-fading-circle .sk-circle6:before {\ -webkit-animation-delay: -0.7s;\ animation-delay: -0.7s;\ }\ .sk-fading-circle .sk-circle7:before {\ -webkit-animation-delay: -0.6s;\ animation-delay: -0.6s;\ }\ .sk-fading-circle .sk-circle8:before {\ -webkit-animation-delay: -0.5s;\ animation-delay: -0.5s;\ }\ .sk-fading-circle .sk-circle9:before {\ -webkit-animation-delay: -0.4s;\ animation-delay: -0.4s;\ }\ .sk-fading-circle .sk-circle10:before {\ -webkit-animation-delay: -0.3s;\ animation-delay: -0.3s;\ }\ .sk-fading-circle .sk-circle11:before {\ -webkit-animation-delay: -0.2s;\ animation-delay: -0.2s;\ }\ .sk-fading-circle .sk-circle12:before {\ -webkit-animation-delay: -0.1s;\ animation-delay: -0.1s;\ }\ @-webkit-keyframes sk-circleFadeDelay {\ 0%, 39%, 100% { opacity: 0; }\ 40% { opacity: 1; }\ }\ @keyframes sk-circleFadeDelay {\ 0%, 39%, 100% { opacity: 0; }\ 40% { opacity: 1; }\ }"; /** * @desc Displays a progress animation at the center of its {@link Canvas} while things are loading or otherwise busy. * * * * Located at {@link Canvas#spinner}. * * Automatically shown while things are loading, however may also be shown by application code wanting to indicate busyness. * * {@link Spinner#processes} holds the count of active processes. As a process starts, it increments {@link Spinner#processes}, then decrements it on completion or failure. * * A Spinner is only visible while {@link Spinner#processes} is greater than zero. * * ````javascript * var spinner = viewer.scene.canvas.spinner; * * // Increment count of busy processes represented by the spinner; * // assuming the count was zero, this now shows the spinner * spinner.processes++; * * // Increment the count again, by some other process; spinner already visible, now requires two decrements * // before it becomes invisible again * spinner.processes++; * * // Decrement the count; count still greater than zero, so spinner remains visible * spinner.process--; * * // Decrement the count; count now zero, so spinner becomes invisible * spinner.process--; * ```` */ class Spinner extends Component { /** @private */ get type() { return "Spinner"; } /** @private */ constructor(owner, cfg = {}) { super(owner, cfg); this._canvas = cfg.canvas; this._element = null; this._isCustom = false; // True when the element is custom HTML if (cfg.elementId) { // Custom spinner element supplied this._element = document.getElementById(cfg.elementId); if (!this._element) { this.error("Can't find given Spinner HTML element: '" + cfg.elementId + "' - will automatically create default element"); } else { this._adjustPosition(); } } if (!this._element) { this._createDefaultSpinner(); } this.processes = 0; } /** @private */ _createDefaultSpinner() { this._injectDefaultCSS(); const element = document.createElement('div'); const style = element.style; style["z-index"] = "9000"; style.position = "absolute"; element.innerHTML = '
\
\
\
\
\
\
\
\
\
\
\
\
\
'; this._canvas.parentElement.appendChild(element); this._element = element; this._isCustom = false; this._adjustPosition(); } /** * @private */ _injectDefaultCSS() { const elementId = "xeokit-spinner-css"; if (document.getElementById(elementId)) { return; } const defaultCSSNode = document.createElement('style'); defaultCSSNode.innerHTML = defaultCSS; defaultCSSNode.id = elementId; document.body.appendChild(defaultCSSNode); } /** * @private */ _adjustPosition() { // (Re)positions spinner DIV over the center of the canvas - called by Canvas if (this._isCustom) { return; } const canvas = this._canvas; const element = this._element; const style = element.style; style["left"] = (canvas.offsetLeft + (canvas.clientWidth * 0.5) - (element.clientWidth * 0.5)) + "px"; style["top"] = (canvas.offsetTop + (canvas.clientHeight * 0.5) - (element.clientHeight * 0.5)) + "px"; } /** * Sets the number of processes this Spinner represents. * * The Spinner is visible while this property is greater than zero. * * Increment this property whenever you commence some process during which you want the Spinner to be visible, then decrement it again when the process is complete. * * Clamps to zero if you attempt to set to to a negative value. * * Fires a {@link Spinner#processes:event} event on change. * Default value is ````0````. * * @param {Number} value New processes count. */ set processes(value) { value = value || 0; if (this._processes === value) { return; } if (value < 0) { return; } const prevValue = this._processes; this._processes = value; const element = this._element; if (element) { element.style["visibility"] = (this._processes > 0) ? "visible" : "hidden"; } /** Fired whenever this Spinner's {@link Spinner#visible} property changes. @event processes @param value The property's new value */ this.fire("processes", this._processes); if (this._processes === 0 && this._processes !== prevValue) { /** Fired whenever this Spinner's {@link Spinner#visible} property becomes zero. @event zeroProcesses */ this.fire("zeroProcesses", this._processes); } } /** * Gets the number of processes this Spinner represents. * * The Spinner is visible while this property is greater than zero. * * @returns {Number} Current processes count. */ get processes() { return this._processes; } _destroy() { if (this._element && (!this._isCustom)) { this._element.parentNode.removeChild(this._element); this._element = null; } const styleElement = document.getElementById("xeokit-spinner-css"); if (styleElement) { styleElement.parentNode.removeChild(styleElement); } } } const WEBGL_CONTEXT_NAMES = [ "webgl2", "experimental-webgl", "webkit-3d", "moz-webgl", "moz-glweb20" ]; /** * @desc Manages its {@link Scene}'s HTML canvas. * * * Provides the HTML canvas element in {@link Canvas#canvas}. * * Has a {@link Spinner}, provided at {@link Canvas#spinner}, which manages the loading progress indicator. */ class Canvas extends Component { /** * @constructor * @private */ constructor(owner, cfg = {}) { super(owner, cfg); this._backgroundColor = math.vec3([ cfg.backgroundColor ? cfg.backgroundColor[0] : 1, cfg.backgroundColor ? cfg.backgroundColor[1] : 1, cfg.backgroundColor ? cfg.backgroundColor[2] : 1]); this._backgroundColorFromAmbientLight = !!cfg.backgroundColorFromAmbientLight; /** * The HTML canvas. * * @property canvas * @type {HTMLCanvasElement} * @final */ this.canvas = cfg.canvas; /** * The WebGL rendering context. * * @property gl * @type {WebGLRenderingContext} * @final */ this.gl = null; /** * True when WebGL 2 support is enabled. * * @property webgl2 * @type {Boolean} * @final */ this.webgl2 = false; // Will set true in _initWebGL if WebGL is requested and we succeed in getting it. /** * Indicates if this Canvas is transparent. * * @property transparent * @type {Boolean} * @default {false} * @final */ this.transparent = !!cfg.transparent; /** * Attributes for the WebGL context * * @type {{}|*} */ this.contextAttr = cfg.contextAttr || {}; this.contextAttr.alpha = this.transparent; this.contextAttr.preserveDrawingBuffer = !!this.contextAttr.preserveDrawingBuffer; this.contextAttr.stencil = false; this.contextAttr.premultipliedAlpha = (!!this.contextAttr.premultipliedAlpha); // False by default: https://github.com/xeokit/xeokit-sdk/issues/251 this.contextAttr.antialias = (this.contextAttr.antialias !== false); // If the canvas uses css styles to specify the sizes make sure the basic // width and height attributes match or the WebGL context will use 300 x 150 this.resolutionScale = cfg.resolutionScale; this.canvas.width = Math.round(this.canvas.clientWidth * this._resolutionScale); this.canvas.height = Math.round(this.canvas.clientHeight * this._resolutionScale); /** * Boundary of the Canvas in absolute browser window coordinates. * * ### Usage: * * ````javascript * var boundary = myScene.canvas.boundary; * * var xmin = boundary[0]; * var ymin = boundary[1]; * var width = boundary[2]; * var height = boundary[3]; * ```` * * @property boundary * @type {Number[]} * @final */ this.boundary = [ this.canvas.offsetLeft, this.canvas.offsetTop, this.canvas.clientWidth, this.canvas.clientHeight ]; // Get WebGL context this._initWebGL(cfg); // Bind context loss and recovery handlers const self = this; this.canvas.addEventListener("webglcontextlost", this._webglcontextlostListener = function (event) { console.time("webglcontextrestored"); self.scene._webglContextLost(); /** * Fired whenever the WebGL context has been lost * @event webglcontextlost */ self.fire("webglcontextlost"); event.preventDefault(); }, false); this.canvas.addEventListener("webglcontextrestored", this._webglcontextrestoredListener = function (event) { self._initWebGL(); if (self.gl) { self.scene._webglContextRestored(self.gl); /** * Fired whenever the WebGL context has been restored again after having previously being lost * @event webglContextRestored * @param value The WebGL context object */ self.fire("webglcontextrestored", self.gl); event.preventDefault(); } console.timeEnd("webglcontextrestored"); }, false); // Attach to resize events on the canvas let dirtyBoundary = true; // make sure we publish the 1st boundary event const resizeObserver = new ResizeObserver((entries) => { for (const entry of entries) { if (entry.contentBoxSize) { dirtyBoundary = true; } } }); resizeObserver.observe(this.canvas); // Publish canvas size and position changes on each scene tick this._tick = this.scene.on("tick", () => { // Only publish if the canvas bounds changed if (!dirtyBoundary) { return; } dirtyBoundary = false; // Set the real size of the canvas (the drawable w*h) self.canvas.width = Math.round(self.canvas.clientWidth * self._resolutionScale); self.canvas.height = Math.round(self.canvas.clientHeight * self._resolutionScale); // Publish the boundary change self.boundary[0] = self.canvas.offsetLeft; self.boundary[1] = self.canvas.offsetTop; self.boundary[2] = self.canvas.clientWidth; self.boundary[3] = self.canvas.clientHeight; self.fire("boundary", self.boundary); }); this._spinner = new Spinner(this.scene, { canvas: this.canvas, elementId: cfg.spinnerElementId }); } /** @private */ get type() { return "Canvas"; } /** * Gets whether the canvas clear color will be derived from {@link AmbientLight} or {@link Canvas#backgroundColor} * when {@link Canvas#transparent} is ```true```. * * When {@link Canvas#transparent} is ```true``` and this is ````true````, then the canvas clear color will * be taken from the {@link Scene}'s ambient light color. * * When {@link Canvas#transparent} is ```true``` and this is ````false````, then the canvas clear color will * be taken from {@link Canvas#backgroundColor}. * * Default value is ````true````. * * @type {Boolean} */ get backgroundColorFromAmbientLight() { return this._backgroundColorFromAmbientLight; } /** * Sets if the canvas background color is derived from an {@link AmbientLight}. * * This only has effect when the canvas is not transparent. When not enabled, the background color * will be the canvas element's HTML/CSS background color. * * Default value is ````true````. * * @type {Boolean} */ set backgroundColorFromAmbientLight(backgroundColorFromAmbientLight) { this._backgroundColorFromAmbientLight = (backgroundColorFromAmbientLight !== false); this.glRedraw(); } /** * Gets the canvas clear color. * * Default value is ````[1, 1, 1]````. * * @type {Number[]} */ get backgroundColor() { return this._backgroundColor; } /** * Sets the canvas clear color. * * Default value is ````[1, 1, 1]````. * * @type {Number[]} */ set backgroundColor(value) { if (value) { this._backgroundColor[0] = value[0]; this._backgroundColor[1] = value[1]; this._backgroundColor[2] = value[2]; } else { this._backgroundColor[0] = 1.0; this._backgroundColor[1] = 1.0; this._backgroundColor[2] = 1.0; } this.glRedraw(); } /** * Gets the scale of the canvas back buffer relative to the CSS-defined size of the canvas. * * This is a common way to trade off rendering quality for speed. If the canvas size is defined in CSS, then * setting this to a value between ````[0..1]```` (eg ````0.5````) will render into a smaller back buffer, giving * a performance boost. * * @returns {*|number} The resolution scale. */ get resolutionScale() { return this._resolutionScale; } /** * Sets the scale of the canvas back buffer relative to the CSS-defined size of the canvas. * * This is a common way to trade off rendering quality for speed. If the canvas size is defined in CSS, then * setting this to a value between ````[0..1]```` (eg ````0.5````) will render into a smaller back buffer, giving * a performance boost. * * @param {*|number} resolutionScale The resolution scale. */ set resolutionScale(resolutionScale) { resolutionScale = resolutionScale || 1.0; if (resolutionScale === this._resolutionScale) { return; } this._resolutionScale = resolutionScale; const canvas = this.canvas; canvas.width = Math.round(canvas.clientWidth * this._resolutionScale); canvas.height = Math.round(canvas.clientHeight * this._resolutionScale); this.glRedraw(); } /** * The busy {@link Spinner} for this Canvas. * * @property spinner * @type Spinner * @final */ get spinner() { return this._spinner; } /** * Creates a default canvas in the DOM. * @private */ _createCanvas() { const canvasId = "xeokit-canvas-" + math.createUUID(); const body = document.getElementsByTagName("body")[0]; const div = document.createElement('div'); const style = div.style; style.height = "100%"; style.width = "100%"; style.padding = "0"; style.margin = "0"; style.background = "rgba(0,0,0,0);"; style.float = "left"; style.left = "0"; style.top = "0"; style.position = "absolute"; style.opacity = "1.0"; style["z-index"] = "-10000"; div.innerHTML += ''; body.appendChild(div); this.canvas = document.getElementById(canvasId); } _getElementXY(e) { let x = 0, y = 0; while (e) { x += (e.offsetLeft - e.scrollLeft); y += (e.offsetTop - e.scrollTop); e = e.offsetParent; } return {x: x, y: y}; } /** * Initialises the WebGL context * @private */ _initWebGL() { // Default context attribute values if (!this.gl) { for (let i = 0; !this.gl && i < WEBGL_CONTEXT_NAMES.length; i++) { try { this.gl = this.canvas.getContext(WEBGL_CONTEXT_NAMES[i], this.contextAttr); } catch (e) { // Try with next context name } } } if (!this.gl) { this.error('Failed to get a WebGL context'); /** * Fired whenever the canvas failed to get a WebGL context, which probably means that WebGL * is either unsupported or has been disabled. * @event webglContextFailed */ this.fire("webglContextFailed", true, true); } // data-textures: avoid to re-bind same texture { const gl = this.gl; let lastTextureUnit = "__"; let originalActiveTexture = gl.activeTexture; gl.activeTexture = function (arg1) { if (lastTextureUnit === arg1) { return; } lastTextureUnit = arg1; originalActiveTexture.call (this, arg1); }; let lastBindTexture = {}; let originalBindTexture = gl.bindTexture; gl.bindTexture = function (arg1, arg2) { if (lastBindTexture[lastTextureUnit] === arg2) { return; } lastBindTexture[lastTextureUnit] = arg2; originalBindTexture.call (this, arg1, arg2); }; // setInterval ( // () => { // console.log (`${avoidedRebinds} avoided texture binds/sec`); // avoidedRebinds = 0; // }, // 1000 // ); } if (this.gl) { // Setup extension (if necessary) and hints for fragment shader derivative functions if (this.webgl2) { this.gl.hint(this.gl.FRAGMENT_SHADER_DERIVATIVE_HINT, this.gl.FASTEST); // data-textures: not using standard-derivatives if (!(this.gl instanceof WebGL2RenderingContext)) ; } } } /** * @private * @deprecated */ getSnapshot(params) { throw "Canvas#getSnapshot() has been replaced by Viewer#getSnapshot() - use that method instead."; } /** * Reads colors of pixels from the last rendered frame. * * Call this method like this: * * ````JavaScript * * // Ignore transparent pixels (default is false) * var opaqueOnly = true; * * var colors = new Float32Array(8); * * viewer.scene.canvas.readPixels([ 100, 22, 12, 33 ], colors, 2, opaqueOnly); * ```` * * Then the r,g,b components of the colors will be set to the colors at those pixels. * * @param {Number[]} pixels * @param {Number[]} colors * @param {Number} size * @param {Boolean} opaqueOnly */ readPixels(pixels, colors, size, opaqueOnly) { return this.scene._renderer.readPixels(pixels, colors, size, opaqueOnly); } /** * Simulates lost WebGL context. */ loseWebGLContext() { if (this.canvas.loseContext) { this.canvas.loseContext(); } } destroy() { this.scene.off(this._tick); this._spinner._destroy(); // Memory leak avoidance this.canvas.removeEventListener("webglcontextlost", this._webglcontextlostListener); this.canvas.removeEventListener("webglcontextrestored", this._webglcontextrestoredListener); this.gl.getExtension("WEBGL_lose_context").loseContext(); this.gl = null; super.destroy(); } } /** * @desc Provides rendering context to {@link Drawable"}s as xeokit renders them for a frame. * * Also creates RTC viewing and picking matrices, caching and reusing matrices within each frame. * * @private */ class FrameContext { constructor(scene) { this._scene = scene; this._matPool = []; this._matPoolNextFreeIndex = 0; this._rtcViewMats = {}; this._rtcPickViewMats = {}; this.reset(); } /** * Called by the renderer before each frame. * @private */ reset() { this._matPoolNextFreeIndex = 0; this._rtcViewMats = {}; this._rtcPickViewMats = {}; /** * The WebGL rendering context. * @type {WebGLRenderingContext} */ this.gl = this._scene.canvas.gl; /** * ID of the last {@link WebGLProgram} that was bound during the current frame. * @property lastProgramId * @type {Number} */ this.lastProgramId = null; /** * Whether to render a physically-based representation for triangle surfaces. * * When ````false````, we'll render them with a fast vertex-shaded Gouraud-shaded representation, which * is great for zillions of objects. * * When ````true````, we'll render them at a better visual quality, using smooth, per-fragment shading * and a more realistic lighting model. * * @property quality * @default false * @type {Boolean} */ this.pbrEnabled = false; /** * Whether to render color textures for triangle surfaces. * * @property quality * @default false * @type {Boolean} */ this.colorTextureEnabled = false; /** * Whether SAO is currently enabled during the current frame. * @property withSAO * @default false * @type {Boolean} */ this.withSAO = false; /** * Whether backfaces are currently enabled during the current frame. * @property backfaces * @default false * @type {Boolean} */ this.backfaces = false; /** * The vertex winding order for what we currently consider to be a backface during current * frame: true == "cw", false == "ccw". * @property frontFace * @default true * @type {Boolean} */ this.frontface = true; /** * The next available texture unit to bind a {@link Texture} to. * @defauilt 0 * @property textureUnit * @type {number} */ this.textureUnit = 0; /** * Performance statistic that counts how many times the renderer has called ````gl.drawElements()```` has been * called so far within the current frame. * @default 0 * @property drawElements * @type {number} */ this.drawElements = 0; /** * Performance statistic that counts how many times ````gl.drawArrays()```` has been called so far within * the current frame. * @default 0 * @property drawArrays * @type {number} */ this.drawArrays = 0; /** * Performance statistic that counts how many times ````gl.useProgram()```` has been called so far within * the current frame. * @default 0 * @property useProgram * @type {number} */ this.useProgram = 0; /** * Statistic that counts how many times ````gl.bindTexture()```` has been called so far within the current frame. * @default 0 * @property bindTexture * @type {number} */ this.bindTexture = 0; /** * Counts how many times the renderer has called ````gl.bindArray()```` so far within the current frame. * @defaulr 0 * @property bindArray * @type {number} */ this.bindArray = 0; /** * Indicates which pass the renderer is currently rendering. * * See {@link Scene/passes:property"}}Scene#passes{{/crossLink}}, which configures how many passes we render * per frame, which typically set to ````2```` when rendering a stereo view. * * @property pass * @type {number} */ this.pass = 0; /** * The 4x4 viewing transform matrix the renderer is currently using when rendering castsShadows. * * This sets the viewpoint to look from the point of view of each {@link DirLight} * or {@link PointLight} that casts a shadow. * * @property shadowViewMatrix * @type {Number[]} */ this.shadowViewMatrix = null; /** * The 4x4 viewing projection matrix the renderer is currently using when rendering shadows. * * @property shadowProjMatrix * @type {Number[]} */ this.shadowProjMatrix = null; /** * The 4x4 viewing transform matrix the renderer is currently using when rendering a ray-pick. * * This sets the viewpoint to look along the ray given to {@link Scene/pick:method"}}Scene#pick(){{/crossLink}} * when picking with a ray. * * @property pickViewMatrix * @type {Number[]} */ this.pickViewMatrix = null; /** * The 4x4 orthographic projection transform matrix the renderer is currently using when rendering a ray-pick. * * @property pickProjMatrix * @type {Number[]} */ this.pickProjMatrix = null; /** * Distance to the near clipping plane when rendering depth fragments for GPU-accelerated 3D picking. * * @property pickZNear * @type {Number|*} */ this.pickZNear = 0.01; /** * Distance to the far clipping plane when rendering depth fragments for GPU-accelerated 3D picking. * * @property pickZFar * @type {Number|*} */ this.pickZFar = 5000; /** * Whether or not the renderer is currently picking invisible objects. * * @property pickInvisible * @type {Number} */ this.pickInvisible = false; /** * Used to draw only requested elements / indices. * * @property pickElementsCount * @type {Number} */ this.pickElementsCount = null; /** * Used to draw only requested elements / indices. * * @property pickElementsOffset * @type {Number} */ this.pickElementsOffset = null; /** The current line width. * * @property lineWidth * @type Number */ this.lineWidth = 1; /** * Collects info from SceneModel.drawSnapInit and SceneModel.drawSnap, * which is then used in Renderer to determine snap-picking results. * * @type {{}} */ this.snapPickLayerParams = {}; /** * Collects info from SceneModel.drawSnapInit and SceneModel.drawSnap, * which is then used in Renderer to determine snap-picking results. * @type {number} */ this.snapPickLayerNumber = 0; /** * Collects info from SceneModel.drawSnapInit and SceneModel.drawSnap, * which is then used in Renderer to determine snap-picking results. * @type {Number[]} */ this.snapPickCoordinateScale = math.vec3(); /** * Collects info from SceneModel.drawSnapInit and SceneModel.drawSnap, * which is then used in Renderer to determine snap-picking results. * @type {Number[]} */ this.snapPickOrigin = math.vec3(); } /** * Get View matrix for the given RTC center. */ getRTCViewMatrix(originHash, origin) { let rtcViewMat = this._rtcViewMats[originHash]; if (!rtcViewMat) { rtcViewMat = this._getNewMat(); createRTCViewMat(this._scene.camera.viewMatrix, origin, rtcViewMat); this._rtcViewMats[originHash] = rtcViewMat; } return rtcViewMat; } /** * Get picking View RTC matrix for the given RTC center. */ getRTCPickViewMatrix(originHash, origin) { let rtcPickViewMat = this._rtcPickViewMats[originHash]; if (!rtcPickViewMat) { rtcPickViewMat = this._getNewMat(); const pickViewMat = this.pickViewMatrix || this._scene.camera.viewMatrix; createRTCViewMat(pickViewMat, origin, rtcPickViewMat); this._rtcPickViewMats[originHash] = rtcPickViewMat; } return rtcPickViewMat; } _getNewMat() { let mat = this._matPool[this._matPoolNextFreeIndex]; if (!mat) { mat = math.mat4(); this._matPool[this._matPoolNextFreeIndex] = mat; } this._matPoolNextFreeIndex++; return mat; } } /** * @private */ class OcclusionLayer { constructor(scene, origin) { this.scene = scene; this.aabb = math.AABB3(); this.origin = math.vec3(origin); this.originHash = this.origin.join(); this.numMarkers = 0; this.markers = {}; this.markerList = []; // Ordered array of Markers this.markerIndices = {}; // ID map of Marker indices in _markerList this.positions = []; // Packed array of World-space marker positions this.indices = []; // Indices corresponding to array above this.positionsBuf = null; this.lenPositionsBuf = 0; this.indicesBuf = null; this.sectionPlanesActive = []; this.culledBySectionPlanes = false; this.occlusionTestList = []; // List of this.lenOcclusionTestList = 0; this.pixels = []; this.aabbDirty = false; this.markerListDirty = false; this.positionsDirty = true; this.occlusionTestListDirty = false; } addMarker(marker) { this.markers[marker.id] = marker; this.markerListDirty = true; this.numMarkers++; } markerWorldPosUpdated(marker) { if (!this.markers[marker.id]) { // Not added return; } const i = this.markerIndices[marker.id]; this.positions[i * 3 + 0] = marker.worldPos[0]; this.positions[i * 3 + 1] = marker.worldPos[1]; this.positions[i * 3 + 2] = marker.worldPos[2]; this.positionsDirty = true; // TODO: avoid reallocating VBO each time } removeMarker(marker) { delete this.markers[marker.id]; this.markerListDirty = true; this.numMarkers--; } update() { if (this.markerListDirty) { this._buildMarkerList(); this.markerListDirty = false; this.positionsDirty = true; this.occlusionTestListDirty = true; } if (this.positionsDirty) { ////////////// TODO: Don't rebuild this when positions change, very wasteful this._buildPositions(); this.positionsDirty = false; this.aabbDirty = true; this.vbosDirty = true; } if (this.aabbDirty) { this._buildAABB(); this.aabbDirty = false; } if (this.vbosDirty) { this._buildVBOs(); this.vbosDirty = false; } if (this.occlusionTestListDirty) { this._buildOcclusionTestList(); } this._updateActiveSectionPlanes(); } _buildMarkerList() { this.numMarkers = 0; for (var id in this.markers) { if (this.markers.hasOwnProperty(id)) { this.markerList[this.numMarkers] = this.markers[id]; this.markerIndices[id] = this.numMarkers; this.numMarkers++; } } this.markerList.length = this.numMarkers; } _buildPositions() { let j = 0; for (let i = 0; i < this.numMarkers; i++) { if (this.markerList[i]) { const marker = this.markerList[i]; const worldPos = marker.rtcPos; this.positions[j++] = worldPos[0]; this.positions[j++] = worldPos[1]; this.positions[j++] = worldPos[2]; this.indices[i] = i; } } this.positions.length = this.numMarkers * 3; this.indices.length = this.numMarkers; } _buildAABB() { const aabb = this.aabb; math.collapseAABB3(aabb); math.expandAABB3Points3(aabb, this.positions); const origin = this.origin; aabb[0] += origin[0]; aabb[1] += origin[1]; aabb[2] += origin[2]; aabb[3] += origin[0]; aabb[4] += origin[1]; aabb[5] += origin[2]; } _buildVBOs() { if (this.positionsBuf) { if (this.lenPositionsBuf === this.positions.length) { // Just updating buffer elements, don't need to reallocate this.positionsBuf.setData(new Float32Array(this.positions)); // Indices don't need updating return; } this.positionsBuf.destroy(); this.positionsBuf = null; this.indicesBuf.destroy(); this.indicesBuf = null; } const gl = this.scene.canvas.gl; const lenPositions = this.numMarkers * 3; const lenIndices = this.numMarkers; this.positionsBuf = new ArrayBuf(gl, gl.ARRAY_BUFFER, new Float32Array(this.positions), lenPositions, 3, gl.STATIC_DRAW); this.indicesBuf = new ArrayBuf(gl, gl.ELEMENT_ARRAY_BUFFER, new Uint16Array(this.indices), lenIndices, 1, gl.STATIC_DRAW); this.lenPositionsBuf = this.positions.length; } _buildOcclusionTestList() { const canvas = this.scene.canvas; const near = this.scene.camera.perspective.near; // Assume near enough to ortho near const boundary = canvas.boundary; const canvasWidth = boundary[2]; const canvasHeight = boundary[3]; let lenPixels = 0; this.lenOcclusionTestList = 0; for (let i = 0; i < this.numMarkers; i++) { const marker = this.markerList[i]; const viewPos = marker.viewPos; if (viewPos[2] > -near) { // Clipped by near plane marker._setVisible(false); continue; } const canvasPos = marker.canvasPos; const canvasX = canvasPos[0]; const canvasY = canvasPos[1]; if ((canvasX + 10) < 0 || (canvasY + 10) < 0 || (canvasX - 10) > canvasWidth || (canvasY - 10) > canvasHeight) { marker._setVisible(false); continue; } if (marker.entity && !marker.entity.visible) { marker._setVisible(false); continue; } if (marker.occludable) { this.occlusionTestList[this.lenOcclusionTestList++] = marker; this.pixels[lenPixels++] = canvasX; this.pixels[lenPixels++] = canvasY; continue; } marker._setVisible(true); } } _updateActiveSectionPlanes() { const sectionPlanes = this.scene._sectionPlanesState.sectionPlanes; const numSectionPlanes = sectionPlanes.length; if (numSectionPlanes > 0) { for (let i = 0; i < numSectionPlanes; i++) { const sectionPlane = sectionPlanes[i]; if (!sectionPlane.active) { this.sectionPlanesActive[i] = false; } else { const intersect = math.planeAABB3Intersect(sectionPlane.dir, sectionPlane.dist, this.aabb); const outside = (intersect === -1); if (outside) { this.culledBySectionPlanes = true; return; } const intersecting = (intersect === 0); this.sectionPlanesActive[i] = intersecting; } } } this.culledBySectionPlanes = false; } destroy() { this.markers = {}; this.markerList.length = 0; if (this.positionsBuf) { this.positionsBuf.destroy(); } if (this.indicesBuf) { this.indicesBuf.destroy(); } } } const MARKER_COLOR = math.vec3([1.0, 0.0, 0.0]); const POINT_SIZE = 20; const tempVec3a$b = math.vec3(); /** * Manages occlusion testing. Private member of a Renderer. * @private */ class OcclusionTester { constructor(scene, renderBufferManager) { this._scene = scene; this._renderBufferManager = renderBufferManager; this._occlusionLayers = {}; this._occlusionLayersList = []; this._occlusionLayersListDirty = false; this._shaderSource = null; this._program = null; this._shaderSourceHash = null; this._shaderSourceDirty = true; // Need to build shader source code ? this._programDirty = false; // Need to build shader program ? this._markersToOcclusionLayersMap = {}; this._onCameraViewMatrix = scene.camera.on("viewMatrix", () => { this._occlusionTestListDirty = true; }); this._onCameraProjMatrix = scene.camera.on("projMatrix", () => { this._occlusionTestListDirty = true; }); this._onCanvasBoundary = scene.canvas.on("boundary", () => { this._occlusionTestListDirty = true; }); } /** * Adds a Marker for occlusion testing. * @param marker */ addMarker(marker) { const originHash = marker.origin.join(); let occlusionLayer = this._occlusionLayers[originHash]; if (!occlusionLayer) { occlusionLayer = new OcclusionLayer(this._scene, marker.origin); this._occlusionLayers[occlusionLayer.originHash] = occlusionLayer; this._occlusionLayersListDirty = true; } occlusionLayer.addMarker(marker); this._markersToOcclusionLayersMap[marker.id] = occlusionLayer; this._occlusionTestListDirty = true; } /** * Notifies OcclusionTester that a Marker has updated its World-space position. * @param marker */ markerWorldPosUpdated(marker) { const occlusionLayer = this._markersToOcclusionLayersMap[marker.id]; if (!occlusionLayer) { return; } const originHash = marker.origin.join(); if (originHash !== occlusionLayer.originHash) { if (occlusionLayer.numMarkers === 1) { occlusionLayer.destroy(); delete this._occlusionLayers[occlusionLayer.originHash]; this._occlusionLayersListDirty = true; } else { occlusionLayer.removeMarker(marker); } let newOcclusionLayer = this._occlusionLayers[originHash]; if (!newOcclusionLayer) { newOcclusionLayer = new OcclusionLayer(this._scene, marker.origin); this._occlusionLayers[originHash] = newOcclusionLayer; this._occlusionLayersListDirty = true; } newOcclusionLayer.addMarker(marker); this._markersToOcclusionLayersMap[marker.id] = newOcclusionLayer; } else { occlusionLayer.markerWorldPosUpdated(marker); } } /** * Removes a Marker from occlusion testing. * @param marker */ removeMarker(marker) { const originHash = marker.origin.join(); let occlusionLayer = this._occlusionLayers[originHash]; if (!occlusionLayer) { return; } if (occlusionLayer.numMarkers === 1) { occlusionLayer.destroy(); delete this._occlusionLayers[occlusionLayer.originHash]; this._occlusionLayersListDirty = true; } else { occlusionLayer.removeMarker(marker); } delete this._markersToOcclusionLayersMap[marker.id]; } /** * Returns true if an occlusion test is needed. * * @returns {Boolean} */ get needOcclusionTest() { return this._occlusionTestListDirty; } /** * Binds the render buffer. After calling this, the caller then renders object silhouettes to the render buffer, * then calls drawMarkers() and doOcclusionTest(). */ bindRenderBuf() { const shaderSourceHash = [this._scene.canvas.canvas.id, this._scene._sectionPlanesState.getHash()].join(";"); if (shaderSourceHash !== this._shaderSourceHash) { this._shaderSourceHash = shaderSourceHash; this._shaderSourceDirty = true; } if (this._shaderSourceDirty) { this._buildShaderSource(); this._shaderSourceDirty = false; this._programDirty = true; } if (this._programDirty) { this._buildProgram(); this._programDirty = false; this._occlusionTestListDirty = true; } if (this._occlusionLayersListDirty) { this._buildOcclusionLayersList(); this._occlusionLayersListDirty = false; } if (this._occlusionTestListDirty) { for (let i = 0, len = this._occlusionLayersList.length; i < len; i++) { const occlusionLayer = this._occlusionLayersList[i]; occlusionLayer.occlusionTestListDirty = true; } this._occlusionTestListDirty = false; } { this._readPixelBuf = this._renderBufferManager.getRenderBuffer("occlusionReadPix"); this._readPixelBuf.bind(); this._readPixelBuf.clear(); } } _buildOcclusionLayersList() { let numOcclusionLayers = 0; for (let originHash in this._occlusionLayers) { if (this._occlusionLayers.hasOwnProperty(originHash)) { this._occlusionLayersList[numOcclusionLayers++] = this._occlusionLayers[originHash]; } } this._occlusionLayersList.length = numOcclusionLayers; } _buildShaderSource() { this._shaderSource = { vertex: this._buildVertexShaderSource(), fragment: this._buildFragmentShaderSource() }; } _buildVertexShaderSource() { const scene = this._scene; const clipping = scene._sectionPlanesState.sectionPlanes.length > 0; const src = []; src.push("#version 300 es"); src.push("// OcclusionTester vertex shader"); src.push("in vec3 position;"); src.push("uniform mat4 modelMatrix;"); src.push("uniform mat4 viewMatrix;"); src.push("uniform mat4 projMatrix;"); if (scene.logarithmicDepthBufferEnabled) { src.push("uniform float logDepthBufFC;"); src.push("out float vFragDepth;"); } if (clipping) { src.push("out vec4 vWorldPosition;"); } src.push("void main(void) {"); src.push("vec4 worldPosition = vec4(position, 1.0); "); src.push(" vec4 viewPosition = viewMatrix * worldPosition;"); if (clipping) { src.push(" vWorldPosition = worldPosition;"); } src.push(" vec4 clipPos = projMatrix * viewPosition;"); src.push(" gl_PointSize = " + POINT_SIZE + ".0;"); if (scene.logarithmicDepthBufferEnabled) { src.push("vFragDepth = 1.0 + clipPos.w;"); } else { if (scene.markerZOffset < 0.000) { src.push("clipPos.z += " + scene.markerZOffset + ";"); } } src.push(" gl_Position = clipPos;"); src.push("}"); return src; } _buildFragmentShaderSource() { const scene = this._scene; const sectionPlanesState = scene._sectionPlanesState; const clipping = sectionPlanesState.sectionPlanes.length > 0; const src = []; src.push("#version 300 es"); src.push("// OcclusionTester fragment shader"); src.push("#ifdef GL_FRAGMENT_PRECISION_HIGH"); src.push("precision highp float;"); src.push("precision highp int;"); src.push("#else"); src.push("precision mediump float;"); src.push("precision mediump int;"); src.push("#endif"); if (scene.logarithmicDepthBufferEnabled) { src.push("uniform float logDepthBufFC;"); src.push("in float vFragDepth;"); } if (clipping) { src.push("in vec4 vWorldPosition;"); for (let i = 0; i < sectionPlanesState.sectionPlanes.length; i++) { src.push("uniform bool sectionPlaneActive" + i + ";"); src.push("uniform vec3 sectionPlanePos" + i + ";"); src.push("uniform vec3 sectionPlaneDir" + i + ";"); } } src.push("out vec4 outColor;"); src.push("void main(void) {"); if (clipping) { src.push(" float dist = 0.0;"); for (var i = 0; i < sectionPlanesState.sectionPlanes.length; i++) { src.push("if (sectionPlaneActive" + i + ") {"); src.push(" dist += clamp(dot(-sectionPlaneDir" + i + ".xyz, vWorldPosition.xyz - sectionPlanePos" + i + ".xyz), 0.0, 1000.0);"); src.push("}"); } src.push(" if (dist > 0.0) { discard; }"); } if (scene.logarithmicDepthBufferEnabled) { src.push("gl_FragDepth = log2( vFragDepth ) * logDepthBufFC * 0.5;"); } src.push(" outColor = vec4(1.0, 0.0, 0.0, 1.0); "); src.push("}"); return src; } _buildProgram() { if (this._program) { this._program.destroy(); } const scene = this._scene; const gl = scene.canvas.gl; const sectionPlanesState = scene._sectionPlanesState; this._program = new Program(gl, this._shaderSource); if (this._program.errors) { this.errors = this._program.errors; return; } const program = this._program; this._uViewMatrix = program.getLocation("viewMatrix"); this._uProjMatrix = program.getLocation("projMatrix"); this._uSectionPlanes = []; const sectionPlanes = sectionPlanesState.sectionPlanes; for (let i = 0, len = sectionPlanes.length; i < len; i++) { this._uSectionPlanes.push({ active: program.getLocation("sectionPlaneActive" + i), pos: program.getLocation("sectionPlanePos" + i), dir: program.getLocation("sectionPlaneDir" + i) }); } this._aPosition = program.getAttribute("position"); if (scene.logarithmicDepthBufferEnabled) { this._uLogDepthBufFC = program.getLocation("logDepthBufFC"); } } /** * Draws {@link Marker}s to the render buffer. */ drawMarkers() { const scene = this._scene; const gl = scene.canvas.gl; const program = this._program; const sectionPlanesState = scene._sectionPlanesState; const camera = scene.camera; const project = scene.camera.project; program.bind(); gl.uniformMatrix4fv(this._uProjMatrix, false, camera._project._state.matrix); if (scene.logarithmicDepthBufferEnabled) { const logDepthBufFC = 2.0 / (Math.log(project.far + 1.0) / Math.LN2); gl.uniform1f(this._uLogDepthBufFC, logDepthBufFC); } for (let i = 0, len = this._occlusionLayersList.length; i < len; i++) { const occlusionLayer = this._occlusionLayersList[i]; occlusionLayer.update(); if (occlusionLayer.culledBySectionPlanes) { continue; } const origin = occlusionLayer.origin; gl.uniformMatrix4fv(this._uViewMatrix, false, createRTCViewMat(camera.viewMatrix, origin)); const numSectionPlanes = sectionPlanesState.sectionPlanes.length; if (numSectionPlanes > 0) { const sectionPlanes = sectionPlanesState.sectionPlanes; for (let sectionPlaneIndex = 0; sectionPlaneIndex < numSectionPlanes; sectionPlaneIndex++) { const sectionPlaneUniforms = this._uSectionPlanes[sectionPlaneIndex]; if (sectionPlaneUniforms) { const active = occlusionLayer.sectionPlanesActive[sectionPlaneIndex]; gl.uniform1i(sectionPlaneUniforms.active, active ? 1 : 0); if (active) { const sectionPlane = sectionPlanes[sectionPlaneIndex]; gl.uniform3fv(sectionPlaneUniforms.pos, getPlaneRTCPos(sectionPlane.dist, sectionPlane.dir, origin, tempVec3a$b)); gl.uniform3fv(sectionPlaneUniforms.dir, sectionPlane.dir); } } } } this._aPosition.bindArrayBuffer(occlusionLayer.positionsBuf); const indicesBuf = occlusionLayer.indicesBuf; indicesBuf.bind(); gl.drawElements(gl.POINTS, indicesBuf.numItems, indicesBuf.itemType, 0); } } /** * Sets visibilities of {@link Marker}s according to whether or not they are obscured by anything in the render buffer. */ doOcclusionTest() { { const resolutionScale = this._scene.canvas.resolutionScale; const markerR = MARKER_COLOR[0] * 255; const markerG = MARKER_COLOR[1] * 255; const markerB = MARKER_COLOR[2] * 255; for (let i = 0, len = this._occlusionLayersList.length; i < len; i++) { const occlusionLayer = this._occlusionLayersList[i]; for (let i = 0; i < occlusionLayer.lenOcclusionTestList; i++) { const marker = occlusionLayer.occlusionTestList[i]; const j = i * 2; const color = this._readPixelBuf.read(Math.round(occlusionLayer.pixels[j] * resolutionScale), Math.round(occlusionLayer.pixels[j + 1] * resolutionScale)); const visible = (color[0] === markerR) && (color[1] === markerG) && (color[2] === markerB); marker._setVisible(visible); } } } } /** * Unbinds render buffer. */ unbindRenderBuf() { { this._readPixelBuf.unbind(); } } /** * Destroys this OcclusionTester. */ destroy() { if (this.destroyed) { return; } for (let i = 0, len = this._occlusionLayersList.length; i < len; i++) { const occlusionLayer = this._occlusionLayersList[i]; occlusionLayer.destroy(); } if (this._program) { this._program.destroy(); } this._scene.camera.off(this._onCameraViewMatrix); this._scene.camera.off(this._onCameraProjMatrix); this._scene.canvas.off(this._onCanvasBoundary); this.destroyed = true; } } const tempVec2 = math.vec2(); /** * SAO implementation inspired from previous SAO work in THREE.js by ludobaka / ludobaka.github.io and bhouston * @private */ class SAOOcclusionRenderer { constructor(scene) { this._scene = scene; this._numSamples = null; // The program this._program = null; this._programError = false; // Variable locations this._aPosition = null; this._aUV = null; this._uDepthTexture = "uDepthTexture"; this._uCameraNear = null; this._uCameraFar = null; this._uCameraProjectionMatrix = null; this._uCameraInverseProjectionMatrix = null; this._uScale = null; this._uIntensity = null; this._uBias = null; this._uKernelRadius = null; this._uMinResolution = null; this._uRandomSeed = null; // VBOs this._uvBuf = null; this._positionsBuf = null; this._indicesBuf = null; this.init(); } init() { this._rebuild = true; } render(depthRenderBuffer) { this._build(); if (this._programError) { return; } if (!this._getInverseProjectMat) { // HACK: scene.camera not defined until render time this._getInverseProjectMat = (() => { let projMatDirty = true; this._scene.camera.on("projMatrix", function () { projMatDirty = true; }); const inverseProjectMat = math.mat4(); return () => { if (projMatDirty) { math.inverseMat4(scene.camera.projMatrix, inverseProjectMat); } return inverseProjectMat; } })(); } const gl = this._scene.canvas.gl; const program = this._program; const scene = this._scene; const sao = scene.sao; const viewportWidth = gl.drawingBufferWidth; const viewportHeight = gl.drawingBufferHeight; const projectState = scene.camera.project._state; const near = projectState.near; const far = projectState.far; const projectionMatrix = projectState.matrix; const inverseProjectionMatrix = this._getInverseProjectMat(); const randomSeed = Math.random(); const perspective = (scene.camera.projection === "perspective"); tempVec2[0] = viewportWidth; tempVec2[1] = viewportHeight; gl.viewport(0, 0, viewportWidth, viewportHeight); gl.clearColor(0, 0, 0, 1); gl.disable(gl.DEPTH_TEST); gl.disable(gl.BLEND); gl.frontFace(gl.CCW); gl.clear(gl.COLOR_BUFFER_BIT); program.bind(); gl.uniform1f(this._uCameraNear, near); gl.uniform1f(this._uCameraFar, far); gl.uniformMatrix4fv(this._uCameraProjectionMatrix, false, projectionMatrix); gl.uniformMatrix4fv(this._uCameraInverseProjectionMatrix, false, inverseProjectionMatrix); gl.uniform1i(this._uPerspective, perspective); gl.uniform1f(this._uScale, sao.scale * (far / 5)); gl.uniform1f(this._uIntensity, sao.intensity); gl.uniform1f(this._uBias, sao.bias); gl.uniform1f(this._uKernelRadius, sao.kernelRadius); gl.uniform1f(this._uMinResolution, sao.minResolution); gl.uniform2fv(this._uViewport, tempVec2); gl.uniform1f(this._uRandomSeed, randomSeed); const depthTexture = depthRenderBuffer.getDepthTexture(); program.bindTexture(this._uDepthTexture, depthTexture, 0); this._aUV.bindArrayBuffer(this._uvBuf); this._aPosition.bindArrayBuffer(this._positionsBuf); this._indicesBuf.bind(); gl.drawElements(gl.TRIANGLES, this._indicesBuf.numItems, this._indicesBuf.itemType, 0); } _build() { let dirty = false; const sao = this._scene.sao; if (sao.numSamples !== this._numSamples) { this._numSamples = Math.floor(sao.numSamples); dirty = true; } if (! (dirty || this._rebuild)) { return; } this._rebuild = false; const gl = this._scene.canvas.gl; if (this._program) { this._program.destroy(); this._program = null; } this._program = new Program(gl, { vertex: [`#version 300 es precision highp float; precision highp int; in vec3 aPosition; in vec2 aUV; out vec2 vUV; void main () { gl_Position = vec4(aPosition, 1.0); vUV = aUV; }`], fragment: [ `#version 300 es precision highp float; precision highp int; #define NORMAL_TEXTURE 0 #define PI 3.14159265359 #define PI2 6.28318530718 #define EPSILON 1e-6 #define NUM_SAMPLES ${this._numSamples} #define NUM_RINGS 4 in vec2 vUV; uniform sampler2D uDepthTexture; uniform float uCameraNear; uniform float uCameraFar; uniform mat4 uProjectMatrix; uniform mat4 uInverseProjectMatrix; uniform bool uPerspective; uniform float uScale; uniform float uIntensity; uniform float uBias; uniform float uKernelRadius; uniform float uMinResolution; uniform vec2 uViewport; uniform float uRandomSeed; float pow2( const in float x ) { return x*x; } highp float rand( const in vec2 uv ) { const highp float a = 12.9898, b = 78.233, c = 43758.5453; highp float dt = dot( uv.xy, vec2( a,b ) ), sn = mod( dt, PI ); return fract(sin(sn) * c); } vec3 packNormalToRGB( const in vec3 normal ) { return normalize( normal ) * 0.5 + 0.5; } vec3 unpackRGBToNormal( const in vec3 rgb ) { return 2.0 * rgb.xyz - 1.0; } const float packUpscale = 256. / 255.; const float unpackDownScale = 255. / 256.; const vec3 packFactors = vec3( 256. * 256. * 256., 256. * 256., 256. ); const vec4 unPackFactors = unpackDownScale / vec4( packFactors, 1. ); const float shiftRights = 1. / 256.; vec4 packFloatToRGBA( const in float v ) { vec4 r = vec4( fract( v * packFactors ), v ); r.yzw -= r.xyz * shiftRights; return r * packUpscale; } float unpackRGBAToFloat( const in vec4 v ) { return dot( floor( v * 255.0 + 0.5 ) / 255.0, unPackFactors ); } float perspectiveDepthToViewZ( const in float invClipZ, const in float near, const in float far ) { return ( near * far ) / ( ( far - near ) * invClipZ - far ); } float orthographicDepthToViewZ( const in float linearClipZ, const in float near, const in float far ) { return linearClipZ * ( near - far ) - near; } float getDepth( const in vec2 screenPosition ) { return vec4(texture(uDepthTexture, screenPosition)).r; } float getViewZ( const in float depth ) { if (uPerspective) { return perspectiveDepthToViewZ( depth, uCameraNear, uCameraFar ); } else { return orthographicDepthToViewZ( depth, uCameraNear, uCameraFar ); } } vec3 getViewPos( const in vec2 screenPos, const in float depth, const in float viewZ ) { float clipW = uProjectMatrix[2][3] * viewZ + uProjectMatrix[3][3]; vec4 clipPosition = vec4( ( vec3( screenPos, depth ) - 0.5 ) * 2.0, 1.0 ); clipPosition *= clipW; return ( uInverseProjectMatrix * clipPosition ).xyz; } vec3 getViewNormal( const in vec3 viewPosition, const in vec2 screenPos ) { return normalize( cross( dFdx( viewPosition ), dFdy( viewPosition ) ) ); } float scaleDividedByCameraFar; float minResolutionMultipliedByCameraFar; float getOcclusion( const in vec3 centerViewPosition, const in vec3 centerViewNormal, const in vec3 sampleViewPosition ) { vec3 viewDelta = sampleViewPosition - centerViewPosition; float viewDistance = length( viewDelta ); float scaledScreenDistance = scaleDividedByCameraFar * viewDistance; return max(0.0, (dot(centerViewNormal, viewDelta) - minResolutionMultipliedByCameraFar) / scaledScreenDistance - uBias) / (1.0 + pow2( scaledScreenDistance ) ); } const float ANGLE_STEP = PI2 * float( NUM_RINGS ) / float( NUM_SAMPLES ); const float INV_NUM_SAMPLES = 1.0 / float( NUM_SAMPLES ); float getAmbientOcclusion( const in vec3 centerViewPosition ) { scaleDividedByCameraFar = uScale / uCameraFar; minResolutionMultipliedByCameraFar = uMinResolution * uCameraFar; vec3 centerViewNormal = getViewNormal( centerViewPosition, vUV ); float angle = rand( vUV + uRandomSeed ) * PI2; vec2 radius = vec2( uKernelRadius * INV_NUM_SAMPLES ) / uViewport; vec2 radiusStep = radius; float occlusionSum = 0.0; float weightSum = 0.0; for( int i = 0; i < NUM_SAMPLES; i ++ ) { vec2 sampleUv = vUV + vec2( cos( angle ), sin( angle ) ) * radius; radius += radiusStep; angle += ANGLE_STEP; float sampleDepth = getDepth( sampleUv ); if( sampleDepth >= ( 1.0 - EPSILON ) ) { continue; } float sampleViewZ = getViewZ( sampleDepth ); vec3 sampleViewPosition = getViewPos( sampleUv, sampleDepth, sampleViewZ ); occlusionSum += getOcclusion( centerViewPosition, centerViewNormal, sampleViewPosition ); weightSum += 1.0; } if( weightSum == 0.0 ) discard; return occlusionSum * ( uIntensity / weightSum ); } out vec4 outColor; void main() { float centerDepth = getDepth( vUV ); if( centerDepth >= ( 1.0 - EPSILON ) ) { discard; } float centerViewZ = getViewZ( centerDepth ); vec3 viewPosition = getViewPos( vUV, centerDepth, centerViewZ ); float ambientOcclusion = getAmbientOcclusion( viewPosition ); outColor = packFloatToRGBA( 1.0- ambientOcclusion ); }`] }); if (this._program.errors) { console.error(this._program.errors.join("\n")); this._programError = true; return; } const uv = new Float32Array([1, 1, 0, 1, 0, 0, 1, 0]); const positions = new Float32Array([1, 1, 0, -1, 1, 0, -1, -1, 0, 1, -1, 0]); // Mitigation: if Uint8Array is used, the geometry is corrupted on OSX when using Chrome with data-textures const indices = new Uint32Array([0, 1, 2, 0, 2, 3]); this._positionsBuf = new ArrayBuf(gl, gl.ARRAY_BUFFER, positions, positions.length, 3, gl.STATIC_DRAW); this._uvBuf = new ArrayBuf(gl, gl.ARRAY_BUFFER, uv, uv.length, 2, gl.STATIC_DRAW); this._indicesBuf = new ArrayBuf(gl, gl.ELEMENT_ARRAY_BUFFER, indices, indices.length, 1, gl.STATIC_DRAW); this._program.bind(); this._uCameraNear = this._program.getLocation("uCameraNear"); this._uCameraFar = this._program.getLocation("uCameraFar"); this._uCameraProjectionMatrix = this._program.getLocation("uProjectMatrix"); this._uCameraInverseProjectionMatrix = this._program.getLocation("uInverseProjectMatrix"); this._uPerspective = this._program.getLocation("uPerspective"); this._uScale = this._program.getLocation("uScale"); this._uIntensity = this._program.getLocation("uIntensity"); this._uBias = this._program.getLocation("uBias"); this._uKernelRadius = this._program.getLocation("uKernelRadius"); this._uMinResolution = this._program.getLocation("uMinResolution"); this._uViewport = this._program.getLocation("uViewport"); this._uRandomSeed = this._program.getLocation("uRandomSeed"); this._aPosition = this._program.getAttribute("aPosition"); this._aUV = this._program.getAttribute("aUV"); this._dirty = false; } destroy() { if (this._program) { this._program.destroy(); this._program = null; } } } const blurStdDev = 4; const blurDepthCutoff = 0.01; const KERNEL_RADIUS = 16; const sampleOffsetsVert = new Float32Array(createSampleOffsets(KERNEL_RADIUS + 1, [0, 1])); const sampleOffsetsHor = new Float32Array(createSampleOffsets(KERNEL_RADIUS + 1, [1, 0])); const sampleWeights = new Float32Array(createSampleWeights(KERNEL_RADIUS + 1, blurStdDev)); const tempVec2a$1 = new Float32Array(2); /** * SAO implementation inspired from previous SAO work in THREE.js by ludobaka / ludobaka.github.io and bhouston * @private */ class SAODepthLimitedBlurRenderer { constructor(scene) { this._scene = scene; // The program this._program = null; this._programError = false; // Variable locations this._aPosition = null; this._aUV = null; this._uDepthTexture = "uDepthTexture"; this._uOcclusionTexture = "uOcclusionTexture"; this._uViewport = null; this._uCameraNear = null; this._uCameraFar = null; this._uCameraProjectionMatrix = null; this._uCameraInverseProjectionMatrix = null; // VBOs this._uvBuf = null; this._positionsBuf = null; this._indicesBuf = null; this.init(); } init() { // Create program & VBOs, locate attributes and uniforms const gl = this._scene.canvas.gl; this._program = new Program(gl, { vertex: [ `#version 300 es precision highp float; precision highp int; in vec3 aPosition; in vec2 aUV; uniform vec2 uViewport; out vec2 vUV; out vec2 vInvSize; void main () { vUV = aUV; vInvSize = 1.0 / uViewport; gl_Position = vec4(aPosition, 1.0); }`], fragment: [ `#version 300 es precision highp float; precision highp int; #define PI 3.14159265359 #define PI2 6.28318530718 #define EPSILON 1e-6 #define KERNEL_RADIUS ${KERNEL_RADIUS} in vec2 vUV; in vec2 vInvSize; uniform sampler2D uDepthTexture; uniform sampler2D uOcclusionTexture; uniform float uCameraNear; uniform float uCameraFar; uniform float uDepthCutoff; uniform vec2 uSampleOffsets[ KERNEL_RADIUS + 1 ]; uniform float uSampleWeights[ KERNEL_RADIUS + 1 ]; const float unpackDownscale = 255. / 256.; const vec3 packFactors = vec3( 256. * 256. * 256., 256. * 256., 256. ); const vec4 unpackFactors = unpackDownscale / vec4( packFactors, 1. ); const float packUpscale = 256. / 255.; const float shiftRights = 1. / 256.; float unpackRGBAToFloat( const in vec4 v ) { return dot( floor( v * 255.0 + 0.5 ) / 255.0, unpackFactors ); } vec4 packFloatToRGBA( const in float v ) { vec4 r = vec4( fract( v * packFactors ), v ); r.yzw -= r.xyz * shiftRights; return r * packUpscale; } float viewZToOrthographicDepth( const in float viewZ) { return ( viewZ + uCameraNear ) / ( uCameraNear - uCameraFar ); } float orthographicDepthToViewZ( const in float linearClipZ) { return linearClipZ * ( uCameraNear - uCameraFar ) - uCameraNear; } float viewZToPerspectiveDepth( const in float viewZ) { return (( uCameraNear + viewZ ) * uCameraFar ) / (( uCameraFar - uCameraNear ) * viewZ ); } float perspectiveDepthToViewZ( const in float invClipZ) { return ( uCameraNear * uCameraFar ) / ( ( uCameraFar - uCameraNear ) * invClipZ - uCameraFar ); } float getDepth( const in vec2 screenPosition ) { return vec4(texture(uDepthTexture, screenPosition)).r; } float getViewZ( const in float depth ) { return perspectiveDepthToViewZ( depth ); } out vec4 outColor; void main() { float depth = getDepth( vUV ); if( depth >= ( 1.0 - EPSILON ) ) { discard; } float centerViewZ = -getViewZ( depth ); bool rBreak = false; bool lBreak = false; float weightSum = uSampleWeights[0]; float occlusionSum = unpackRGBAToFloat(texture( uOcclusionTexture, vUV )) * weightSum; for( int i = 1; i <= KERNEL_RADIUS; i ++ ) { float sampleWeight = uSampleWeights[i]; vec2 sampleUVOffset = uSampleOffsets[i] * vInvSize; vec2 sampleUV = vUV + sampleUVOffset; float viewZ = -getViewZ( getDepth( sampleUV ) ); if( abs( viewZ - centerViewZ ) > uDepthCutoff ) { rBreak = true; } if( ! rBreak ) { occlusionSum += unpackRGBAToFloat(texture( uOcclusionTexture, sampleUV )) * sampleWeight; weightSum += sampleWeight; } sampleUV = vUV - sampleUVOffset; viewZ = -getViewZ( getDepth( sampleUV ) ); if( abs( viewZ - centerViewZ ) > uDepthCutoff ) { lBreak = true; } if( ! lBreak ) { occlusionSum += unpackRGBAToFloat(texture( uOcclusionTexture, sampleUV )) * sampleWeight; weightSum += sampleWeight; } } outColor = packFloatToRGBA(occlusionSum / weightSum); }` ] }); if (this._program.errors) { console.error(this._program.errors.join("\n")); this._programError = true; return; } const uv = new Float32Array([1, 1, 0, 1, 0, 0, 1, 0]); const positions = new Float32Array([1, 1, 0, -1, 1, 0, -1, -1, 0, 1, -1, 0]); // Mitigation: if Uint8Array is used, the geometry is corrupted on OSX when using Chrome with data-textures const indices = new Uint32Array([0, 1, 2, 0, 2, 3]); this._positionsBuf = new ArrayBuf(gl, gl.ARRAY_BUFFER, positions, positions.length, 3, gl.STATIC_DRAW); this._uvBuf = new ArrayBuf(gl, gl.ARRAY_BUFFER, uv, uv.length, 2, gl.STATIC_DRAW); this._indicesBuf = new ArrayBuf(gl, gl.ELEMENT_ARRAY_BUFFER, indices, indices.length, 1, gl.STATIC_DRAW); this._program.bind(); this._uViewport = this._program.getLocation("uViewport"); this._uCameraNear = this._program.getLocation("uCameraNear"); this._uCameraFar = this._program.getLocation("uCameraFar"); this._uDepthCutoff = this._program.getLocation("uDepthCutoff"); this._uSampleOffsets = gl.getUniformLocation(this._program.handle, "uSampleOffsets"); this._uSampleWeights = gl.getUniformLocation(this._program.handle, "uSampleWeights"); this._aPosition = this._program.getAttribute("aPosition"); this._aUV = this._program.getAttribute("aUV"); } render(depthRenderBuffer, occlusionRenderBuffer, direction) { if (this._programError) { return; } if (!this._getInverseProjectMat) { // HACK: scene.camera not defined until render time this._getInverseProjectMat = (() => { let projMatDirty = true; this._scene.camera.on("projMatrix", function () { projMatDirty = true; }); const inverseProjectMat = math.mat4(); return () => { if (projMatDirty) { math.inverseMat4(scene.camera.projMatrix, inverseProjectMat); } return inverseProjectMat; } })(); } const gl = this._scene.canvas.gl; const program = this._program; const scene = this._scene; const viewportWidth = gl.drawingBufferWidth; const viewportHeight = gl.drawingBufferHeight; const projectState = scene.camera.project._state; const near = projectState.near; const far = projectState.far; gl.viewport(0, 0, viewportWidth, viewportHeight); gl.clearColor(0, 0, 0, 1); gl.enable(gl.DEPTH_TEST); gl.disable(gl.BLEND); gl.frontFace(gl.CCW); gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); program.bind(); tempVec2a$1[0] = viewportWidth; tempVec2a$1[1] = viewportHeight; gl.uniform2fv(this._uViewport, tempVec2a$1); gl.uniform1f(this._uCameraNear, near); gl.uniform1f(this._uCameraFar, far); gl.uniform1f(this._uDepthCutoff, blurDepthCutoff); if (direction === 0) {// Horizontal gl.uniform2fv(this._uSampleOffsets, sampleOffsetsHor); } else { // Vertical gl.uniform2fv(this._uSampleOffsets, sampleOffsetsVert); } gl.uniform1fv(this._uSampleWeights, sampleWeights); const depthTexture = depthRenderBuffer.getDepthTexture(); const occlusionTexture = occlusionRenderBuffer.getTexture(); program.bindTexture(this._uDepthTexture, depthTexture, 0); // TODO: use FrameCtx.textureUnit program.bindTexture(this._uOcclusionTexture, occlusionTexture, 1); this._aUV.bindArrayBuffer(this._uvBuf); this._aPosition.bindArrayBuffer(this._positionsBuf); this._indicesBuf.bind(); gl.drawElements(gl.TRIANGLES, this._indicesBuf.numItems, this._indicesBuf.itemType, 0); } destroy() { this._program.destroy(); } } function createSampleWeights(kernelRadius, stdDev) { const weights = []; for (let i = 0; i <= kernelRadius; i++) { weights.push(gaussian(i, stdDev)); } return weights; // TODO: Optimize } function gaussian(x, stdDev) { return Math.exp(-(x * x) / (2.0 * (stdDev * stdDev))) / (Math.sqrt(2.0 * Math.PI) * stdDev); } function createSampleOffsets(kernelRadius, uvIncrement) { const offsets = []; for (let i = 0; i <= kernelRadius; i++) { offsets.push(uvIncrement[0] * i); offsets.push(uvIncrement[1] * i); } return offsets; } /** * @private */ class RenderBufferManager { constructor(scene) { this.scene = scene; this._renderBuffersBasic = {}; this._renderBuffersScaled = {}; } getRenderBuffer(id, options) { const renderBuffers = (this.scene.canvas.resolutionScale === 1.0) ? this._renderBuffersBasic : this._renderBuffersScaled; let renderBuffer = renderBuffers[id]; if (!renderBuffer) { renderBuffer = new RenderBuffer(this.scene.canvas.canvas, this.scene.canvas.gl, options); renderBuffers[id] = renderBuffer; } return renderBuffer; } destroy() { for (let id in this._renderBuffersBasic) { this._renderBuffersBasic[id].destroy(); } for (let id in this._renderBuffersScaled) { this._renderBuffersScaled[id].destroy(); } } } const vec3_0 = math.vec3([0,0,0]); /** * @private */ const Renderer$1 = function (scene, options) { options = options || {}; const frameCtx = new FrameContext(scene); const canvas = scene.canvas.canvas; /** * @type {WebGL2RenderingContext} */ const gl = scene.canvas.gl; const canvasTransparent = (!!options.transparent); const alphaDepthMask = options.alphaDepthMask; const pickIDs = new Map$1({}); let drawableTypeInfo = {}; let drawables = {}; let postSortDrawableList = []; let postCullDrawableList = []; let uiDrawableList = []; let drawableListDirty = true; let stateSortDirty = true; let imageDirty = true; let transparentEnabled = true; let edgesEnabled = true; let saoEnabled = true; let pbrEnabled = true; let colorTextureEnabled = true; const renderBufferManager = new RenderBufferManager(scene); let snapshotBound = false; const saoOcclusionRenderer = new SAOOcclusionRenderer(scene); const saoDepthLimitedBlurRenderer = new SAODepthLimitedBlurRenderer(scene); this.scene = scene; this._occlusionTester = null; // Lazy-created in #addMarker() this.capabilities = { astcSupported: !!getExtension(gl, 'WEBGL_compressed_texture_astc'), etc1Supported: true, // WebGL2 etc2Supported: !!getExtension(gl, 'WEBGL_compressed_texture_etc'), dxtSupported: !!getExtension(gl, 'WEBGL_compressed_texture_s3tc'), bptcSupported: !!getExtension(gl, 'EXT_texture_compression_bptc'), pvrtcSupported: !!(getExtension(gl, 'WEBGL_compressed_texture_pvrtc') || getExtension(gl, 'WEBKIT_WEBGL_compressed_texture_pvrtc')) }; this.setTransparentEnabled = function (enabled) { transparentEnabled = enabled; imageDirty = true; }; this.setEdgesEnabled = function (enabled) { edgesEnabled = enabled; imageDirty = true; }; this.setSAOEnabled = function (enabled) { saoEnabled = enabled; imageDirty = true; }; this.setPBREnabled = function (enabled) { pbrEnabled = enabled; imageDirty = true; }; this.setColorTextureEnabled = function (enabled) { colorTextureEnabled = enabled; imageDirty = true; }; this.needStateSort = function () { stateSortDirty = true; }; this.shadowsDirty = function () { }; this.imageDirty = function () { imageDirty = true; }; this.webglContextLost = function () { }; this.webglContextRestored = function (gl) { // renderBufferManager.webglContextRestored(gl); saoOcclusionRenderer.init(); saoDepthLimitedBlurRenderer.init(); imageDirty = true; }; /** * Inserts a drawable into this renderer. * @private */ this.addDrawable = function (id, drawable) { const type = drawable.type; if (!type) { console.error("Renderer#addDrawable() : drawable with ID " + id + " has no 'type' - ignoring"); return; } let drawableInfo = drawableTypeInfo[type]; if (!drawableInfo) { drawableInfo = { type: drawable.type, count: 0, isStateSortable: drawable.isStateSortable, stateSortCompare: drawable.stateSortCompare, drawableMap: {}, drawableListPreCull: [], drawableList: [] }; drawableTypeInfo[type] = drawableInfo; } drawableInfo.count++; drawableInfo.drawableMap[id] = drawable; drawables[id] = drawable; drawableListDirty = true; }; /** * Removes a drawable from this renderer. * @private */ this.removeDrawable = function (id) { const drawable = drawables[id]; if (!drawable) { console.error("Renderer#removeDrawable() : drawable not found with ID " + id + " - ignoring"); return; } const type = drawable.type; const drawableInfo = drawableTypeInfo[type]; if (--drawableInfo.count <= 0) { delete drawableTypeInfo[type]; } else { delete drawableInfo.drawableMap[id]; } delete drawables[id]; drawableListDirty = true; }; /** * Gets a unique pick ID for the given Pickable. A Pickable can be a {@link Mesh} or a {@link PerformanceMesh}. * @returns {Number} New pick ID. */ this.getPickID = function (entity) { return pickIDs.addItem(entity); }; /** * Released a pick ID for reuse. * @param {Number} pickID Pick ID to release. */ this.putPickID = function (pickID) { pickIDs.removeItem(pickID); }; /** * Clears the canvas. * @private */ this.clear = function (params) { gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight); if (canvasTransparent) { gl.clearColor(1, 1, 1, 1); } else { const backgroundColor = scene.canvas.backgroundColorFromAmbientLight ? this.lights.getAmbientColorAndIntensity() : scene.canvas.backgroundColor; gl.clearColor(backgroundColor[0], backgroundColor[1], backgroundColor[2], 1.0); } gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); }; /** * Returns true if the next call to render() will draw something * @returns {Boolean} */ this.needsRender = function () { const needsRender = (imageDirty || drawableListDirty || stateSortDirty); return needsRender; }; /** * Renders inserted drawables. * @private */ this.render = function (params) { params = params || {}; if (params.force) { imageDirty = true; } updateDrawlist(); if (imageDirty) { draw(params); stats.frame.frameCount++; imageDirty = false; } }; function updateDrawlist() { // Prepares state-sorted array of drawables from maps of inserted drawables if (drawableListDirty) { buildDrawableList(); drawableListDirty = false; stateSortDirty = true; } if (stateSortDirty) { sortDrawableList(); stateSortDirty = false; imageDirty = true; } if (imageDirty) { // Image is usually dirty because the camera moved cullDrawableList(); } } function buildDrawableList() { for (let type in drawableTypeInfo) { if (drawableTypeInfo.hasOwnProperty(type)) { const drawableInfo = drawableTypeInfo[type]; const drawableMap = drawableInfo.drawableMap; const drawableListPreCull = drawableInfo.drawableListPreCull; let lenDrawableList = 0; for (let id in drawableMap) { if (drawableMap.hasOwnProperty(id)) { drawableListPreCull[lenDrawableList++] = drawableMap[id]; } } drawableListPreCull.length = lenDrawableList; } } } function sortDrawableList() { let lenDrawableList = 0; for (let type in drawableTypeInfo) { if (drawableTypeInfo.hasOwnProperty(type)) { const drawableInfo = drawableTypeInfo[type]; const drawableListPreCull = drawableInfo.drawableListPreCull; for (let i = 0, len = drawableListPreCull.length; i < len; i++) { const drawable = drawableListPreCull[i]; postSortDrawableList[lenDrawableList++] = drawable; } } } postSortDrawableList.length = lenDrawableList; postSortDrawableList.sort((a, b) => { return a.renderOrder - b.renderOrder; }); } function cullDrawableList() { let lenDrawableList = 0; let lenUiList = 0; for (let i = 0, len = postSortDrawableList.length; i < len; i++) { const drawable = postSortDrawableList[i]; drawable.rebuildRenderFlags(); if (!drawable.renderFlags.culled) { if (drawable.isUI) { uiDrawableList[lenUiList++] = drawable; } else { postCullDrawableList[lenDrawableList++] = drawable; } } } postCullDrawableList.length = lenDrawableList; uiDrawableList.length = lenUiList; } function draw(params) { const sao = scene.sao; if (saoEnabled && sao.possible) { drawSAOBuffers(params); } drawShadowMaps(); drawColor(params); } function drawSAOBuffers(params) { const sao = scene.sao; // Render depth buffer const saoDepthRenderBuffer = renderBufferManager.getRenderBuffer("saoDepth", { depthTexture: true }); saoDepthRenderBuffer.bind(); saoDepthRenderBuffer.clear(); drawDepth(params); saoDepthRenderBuffer.unbind(); // Render occlusion buffer const occlusionRenderBuffer1 = renderBufferManager.getRenderBuffer("saoOcclusion"); occlusionRenderBuffer1.bind(); occlusionRenderBuffer1.clear(); saoOcclusionRenderer.render(saoDepthRenderBuffer); occlusionRenderBuffer1.unbind(); if (sao.blur) { // Horizontally blur occlusion buffer 1 into occlusion buffer 2 const occlusionRenderBuffer2 = renderBufferManager.getRenderBuffer("saoOcclusion2"); occlusionRenderBuffer2.bind(); occlusionRenderBuffer2.clear(); saoDepthLimitedBlurRenderer.render(saoDepthRenderBuffer, occlusionRenderBuffer1, 0); occlusionRenderBuffer2.unbind(); // Vertically blur occlusion buffer 2 back into occlusion buffer 1 occlusionRenderBuffer1.bind(); occlusionRenderBuffer1.clear(); saoDepthLimitedBlurRenderer.render(saoDepthRenderBuffer, occlusionRenderBuffer2, 1); occlusionRenderBuffer1.unbind(); } } function drawDepth(params) { frameCtx.reset(); frameCtx.pass = params.pass; gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight); gl.clearColor(0, 0, 0, 0); gl.enable(gl.DEPTH_TEST); gl.frontFace(gl.CCW); gl.enable(gl.CULL_FACE); gl.depthMask(true); if (params.clear !== false) { gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); } for (let i = 0, len = postCullDrawableList.length; i < len; i++) { const drawable = postCullDrawableList[i]; if (drawable.culled === true || drawable.visible === false || !drawable.drawDepth || !drawable.saoEnabled) { continue; } if (drawable.renderFlags.colorOpaque) { drawable.drawDepth(frameCtx); } } // const numVertexAttribs = WEBGL_INFO.MAX_VERTEX_ATTRIBS; // Fixes https://github.com/xeokit/xeokit-sdk/issues/174 // for (let ii = 0; ii < numVertexAttribs; ii++) { // gl.disableVertexAttribArray(ii); // } } function drawShadowMaps() { let lights = scene._lightsState.lights; for (let i = 0, len = lights.length; i < len; i++) { const light = lights[i]; if (!light.castsShadow) { continue; } drawShadowMap(light); } } function drawShadowMap(light) { const castsShadow = light.castsShadow; if (!castsShadow) { return; } const shadowRenderBuf = light.getShadowRenderBuf(); if (!shadowRenderBuf) { return; } shadowRenderBuf.bind(); frameCtx.reset(); frameCtx.backfaces = true; frameCtx.frontface = true; frameCtx.shadowViewMatrix = light.getShadowViewMatrix(); frameCtx.shadowProjMatrix = light.getShadowProjMatrix(); gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight); gl.clearColor(0, 0, 0, 1); gl.enable(gl.DEPTH_TEST); gl.disable(gl.BLEND); gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); for (let type in drawableTypeInfo) { if (drawableTypeInfo.hasOwnProperty(type)) { const drawableInfo = drawableTypeInfo[type]; const drawableList = drawableInfo.drawableList; for (let i = 0, len = drawableList.length; i < len; i++) { const drawable = drawableList[i]; if (drawable.visible === false || !drawable.castsShadow || !drawable.drawShadow) { continue; } if (drawable.renderFlags.colorOpaque) { // Transparent objects don't cast shadows (yet) drawable.drawShadow(frameCtx); } } } } shadowRenderBuf.unbind(); } function drawColor(params) { const normalDrawSAOBin = []; const normalEdgesOpaqueBin = []; const normalFillTransparentBin = []; const normalEdgesTransparentBin = []; const xrayedFillOpaqueBin = []; const xrayEdgesOpaqueBin = []; const xrayedFillTransparentBin = []; const xrayEdgesTransparentBin = []; const highlightedFillOpaqueBin = []; const highlightedEdgesOpaqueBin = []; const highlightedFillTransparentBin = []; const highlightedEdgesTransparentBin = []; const selectedFillOpaqueBin = []; const selectedEdgesOpaqueBin = []; const selectedFillTransparentBin = []; const selectedEdgesTransparentBin = []; const ambientColorAndIntensity = scene._lightsState.getAmbientColorAndIntensity(); frameCtx.reset(); frameCtx.pass = params.pass; frameCtx.withSAO = false; frameCtx.pbrEnabled = pbrEnabled && !!scene.pbrEnabled; frameCtx.colorTextureEnabled = colorTextureEnabled && !!scene.colorTextureEnabled; gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight); if (canvasTransparent) { gl.clearColor(0, 0, 0, 0); } else { const backgroundColor = scene.canvas.backgroundColorFromAmbientLight ? ambientColorAndIntensity : scene.canvas.backgroundColor; gl.clearColor(backgroundColor[0], backgroundColor[1], backgroundColor[2], 1.0); } gl.enable(gl.DEPTH_TEST); gl.frontFace(gl.CCW); gl.enable(gl.CULL_FACE); gl.depthMask(true); gl.lineWidth(1); frameCtx.lineWidth = 1; const saoPossible = scene.sao.possible; if (saoEnabled && saoPossible) { const occlusionRenderBuffer1 = renderBufferManager.getRenderBuffer("saoOcclusion"); frameCtx.occlusionTexture = occlusionRenderBuffer1 ? occlusionRenderBuffer1.getTexture() : null; } else { frameCtx.occlusionTexture = null; } let i; let drawable; const startTime = Date.now(); if (params.clear !== false) { gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); } const renderDrawables = function(drawables) { let normalDrawSAOBinLen = 0; let normalEdgesOpaqueBinLen = 0; let normalFillTransparentBinLen = 0; let normalEdgesTransparentBinLen = 0; let xrayedFillOpaqueBinLen = 0; let xrayEdgesOpaqueBinLen = 0; let xrayedFillTransparentBinLen = 0; let xrayEdgesTransparentBinLen = 0; let highlightedFillOpaqueBinLen = 0; let highlightedEdgesOpaqueBinLen = 0; let highlightedFillTransparentBinLen = 0; let highlightedEdgesTransparentBinLen = 0; let selectedFillOpaqueBinLen = 0; let selectedEdgesOpaqueBinLen = 0; let selectedFillTransparentBinLen = 0; let selectedEdgesTransparentBinLen = 0; //------------------------------------------------------------------------------------------------------ // Render normal opaque solids, defer others to bins to render after //------------------------------------------------------------------------------------------------------ for (let i = 0, len = drawables.length; i < len; i++) { drawable = drawables[i]; if (drawable.culled === true || drawable.visible === false) { continue; } const renderFlags = drawable.renderFlags; if (renderFlags.colorOpaque) { if (saoEnabled && saoPossible && drawable.saoEnabled) { normalDrawSAOBin[normalDrawSAOBinLen++] = drawable; } else { drawable.drawColorOpaque(frameCtx); } } if (transparentEnabled) { if (renderFlags.colorTransparent) { normalFillTransparentBin[normalFillTransparentBinLen++] = drawable; } } if (renderFlags.xrayedSilhouetteTransparent) { xrayedFillTransparentBin[xrayedFillTransparentBinLen++] = drawable; } if (renderFlags.xrayedSilhouetteOpaque) { xrayedFillOpaqueBin[xrayedFillOpaqueBinLen++] = drawable; } if (renderFlags.highlightedSilhouetteTransparent) { highlightedFillTransparentBin[highlightedFillTransparentBinLen++] = drawable; } if (renderFlags.highlightedSilhouetteOpaque) { highlightedFillOpaqueBin[highlightedFillOpaqueBinLen++] = drawable; } if (renderFlags.selectedSilhouetteTransparent) { selectedFillTransparentBin[selectedFillTransparentBinLen++] = drawable; } if (renderFlags.selectedSilhouetteOpaque) { selectedFillOpaqueBin[selectedFillOpaqueBinLen++] = drawable; } if (drawable.edges && edgesEnabled) { if (renderFlags.edgesOpaque) { normalEdgesOpaqueBin[normalEdgesOpaqueBinLen++] = drawable; } if (renderFlags.edgesTransparent) { normalEdgesTransparentBin[normalEdgesTransparentBinLen++] = drawable; } if (renderFlags.selectedEdgesTransparent) { selectedEdgesTransparentBin[selectedEdgesTransparentBinLen++] = drawable; } if (renderFlags.selectedEdgesOpaque) { selectedEdgesOpaqueBin[selectedEdgesOpaqueBinLen++] = drawable; } if (renderFlags.xrayedEdgesTransparent) { xrayEdgesTransparentBin[xrayEdgesTransparentBinLen++] = drawable; } if (renderFlags.xrayedEdgesOpaque) { xrayEdgesOpaqueBin[xrayEdgesOpaqueBinLen++] = drawable; } if (renderFlags.highlightedEdgesTransparent) { highlightedEdgesTransparentBin[highlightedEdgesTransparentBinLen++] = drawable; } if (renderFlags.highlightedEdgesOpaque) { highlightedEdgesOpaqueBin[highlightedEdgesOpaqueBinLen++] = drawable; } } } //------------------------------------------------------------------------------------------------------ // Render deferred bins //------------------------------------------------------------------------------------------------------ // Opaque color with SAO if (normalDrawSAOBinLen > 0) { frameCtx.withSAO = true; for (i = 0; i < normalDrawSAOBinLen; i++) { normalDrawSAOBin[i].drawColorOpaque(frameCtx); } } // Opaque edges if (normalEdgesOpaqueBinLen > 0) { for (i = 0; i < normalEdgesOpaqueBinLen; i++) { normalEdgesOpaqueBin[i].drawEdgesColorOpaque(frameCtx); } } // Opaque X-ray fill if (xrayedFillOpaqueBinLen > 0) { for (i = 0; i < xrayedFillOpaqueBinLen; i++) { xrayedFillOpaqueBin[i].drawSilhouetteXRayed(frameCtx); } } // Opaque X-ray edges if (xrayEdgesOpaqueBinLen > 0) { for (i = 0; i < xrayEdgesOpaqueBinLen; i++) { xrayEdgesOpaqueBin[i].drawEdgesXRayed(frameCtx); } } // Transparent if (xrayedFillTransparentBinLen > 0 || xrayEdgesTransparentBinLen > 0 || normalFillTransparentBinLen > 0 || normalEdgesTransparentBinLen > 0) { gl.enable(gl.CULL_FACE); gl.enable(gl.BLEND); if (canvasTransparent) { gl.blendEquation(gl.FUNC_ADD); gl.blendFuncSeparate(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA, gl.ONE, gl.ONE_MINUS_SRC_ALPHA); } else { gl.blendEquation(gl.FUNC_ADD); gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA); } frameCtx.backfaces = false; if (!alphaDepthMask) { gl.depthMask(false); } // Transparent color edges if (normalFillTransparentBinLen > 0 || normalEdgesTransparentBinLen > 0) { gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA); } if (normalEdgesTransparentBinLen > 0) { for (i = 0; i < normalEdgesTransparentBinLen; i++) { drawable = normalEdgesTransparentBin[i]; drawable.drawEdgesColorTransparent(frameCtx); } } // Transparent color fill if (normalFillTransparentBinLen > 0) { const eye = frameCtx.pickOrigin || scene.camera.eye; const byDist = normalFillTransparentBin.map(d => ({ drawable: d, distSq: math.distVec3(d.origin || vec3_0, eye) })); byDist.sort((a, b) => b.distSq - a.distSq); for (i = 0; i < normalFillTransparentBinLen; i++) { byDist[i].drawable.drawColorTransparent(frameCtx); } } // Transparent X-ray edges if (xrayEdgesTransparentBinLen > 0) { for (i = 0; i < xrayEdgesTransparentBinLen; i++) { xrayEdgesTransparentBin[i].drawEdgesXRayed(frameCtx); } } // Transparent X-ray fill if (xrayedFillTransparentBinLen > 0) { for (i = 0; i < xrayedFillTransparentBinLen; i++) { xrayedFillTransparentBin[i].drawSilhouetteXRayed(frameCtx); } } gl.disable(gl.BLEND); if (!alphaDepthMask) { gl.depthMask(true); } } // Opaque highlight if (highlightedFillOpaqueBinLen > 0 || highlightedEdgesOpaqueBinLen > 0) { frameCtx.lastProgramId = null; if (scene.highlightMaterial.glowThrough) { gl.clear(gl.DEPTH_BUFFER_BIT); } // Opaque highlighted edges if (highlightedEdgesOpaqueBinLen > 0) { for (i = 0; i < highlightedEdgesOpaqueBinLen; i++) { highlightedEdgesOpaqueBin[i].drawEdgesHighlighted(frameCtx); } } // Opaque highlighted fill if (highlightedFillOpaqueBinLen > 0) { for (i = 0; i < highlightedFillOpaqueBinLen; i++) { highlightedFillOpaqueBin[i].drawSilhouetteHighlighted(frameCtx); } } } // Highlighted transparent if (highlightedFillTransparentBinLen > 0 || highlightedEdgesTransparentBinLen > 0 || highlightedFillOpaqueBinLen > 0) { frameCtx.lastProgramId = null; if (scene.selectedMaterial.glowThrough) { gl.clear(gl.DEPTH_BUFFER_BIT); } gl.enable(gl.BLEND); if (canvasTransparent) { gl.blendEquation(gl.FUNC_ADD); gl.blendFuncSeparate(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA, gl.ONE, gl.ONE_MINUS_SRC_ALPHA); } else { gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA); } gl.enable(gl.CULL_FACE); // Highlighted transparent edges if (highlightedEdgesTransparentBinLen > 0) { for (i = 0; i < highlightedEdgesTransparentBinLen; i++) { highlightedEdgesTransparentBin[i].drawEdgesHighlighted(frameCtx); } } // Highlighted transparent fill if (highlightedFillTransparentBinLen > 0) { for (i = 0; i < highlightedFillTransparentBinLen; i++) { highlightedFillTransparentBin[i].drawSilhouetteHighlighted(frameCtx); } } gl.disable(gl.BLEND); } // Selected opaque if (selectedFillOpaqueBinLen > 0 || selectedEdgesOpaqueBinLen > 0) { frameCtx.lastProgramId = null; if (scene.selectedMaterial.glowThrough) { gl.clear(gl.DEPTH_BUFFER_BIT); } // Selected opaque fill if (selectedEdgesOpaqueBinLen > 0) { for (i = 0; i < selectedEdgesOpaqueBinLen; i++) { selectedEdgesOpaqueBin[i].drawEdgesSelected(frameCtx); } } // Selected opaque edges if (selectedFillOpaqueBinLen > 0) { for (i = 0; i < selectedFillOpaqueBinLen; i++) { selectedFillOpaqueBin[i].drawSilhouetteSelected(frameCtx); } } } // Selected transparent if (selectedFillTransparentBinLen > 0 || selectedEdgesTransparentBinLen > 0) { frameCtx.lastProgramId = null; if (scene.selectedMaterial.glowThrough) { gl.clear(gl.DEPTH_BUFFER_BIT); } gl.enable(gl.CULL_FACE); gl.enable(gl.BLEND); if (canvasTransparent) { gl.blendEquation(gl.FUNC_ADD); gl.blendFuncSeparate(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA, gl.ONE, gl.ONE_MINUS_SRC_ALPHA); } else { gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA); } // Selected transparent edges if (selectedEdgesTransparentBinLen > 0) { for (i = 0; i < selectedEdgesTransparentBinLen; i++) { selectedEdgesTransparentBin[i].drawEdgesSelected(frameCtx); } } // Selected transparent fill if (selectedFillTransparentBinLen > 0) { for (i = 0; i < selectedFillTransparentBinLen; i++) { selectedFillTransparentBin[i].drawSilhouetteSelected(frameCtx); } } gl.disable(gl.BLEND); } }; renderDrawables(postCullDrawableList); if (uiDrawableList.length > 0) { gl.clear(gl.DEPTH_BUFFER_BIT); renderDrawables(uiDrawableList); } const endTime = Date.now(); const frameStats = stats.frame; frameStats.renderTime = (endTime - startTime) / 1000.0; frameStats.drawElements = frameCtx.drawElements; frameStats.drawArrays = frameCtx.drawArrays; frameStats.useProgram = frameCtx.useProgram; frameStats.bindTexture = frameCtx.bindTexture; frameStats.bindArray = frameCtx.bindArray; const numTextureUnits = WEBGL_INFO.MAX_TEXTURE_IMAGE_UNITS; for (let ii = 0; ii < numTextureUnits; ii++) { gl.activeTexture(gl.TEXTURE0 + ii); } gl.bindTexture(gl.TEXTURE_CUBE_MAP, null); gl.bindTexture(gl.TEXTURE_2D, null); const numVertexAttribs = WEBGL_INFO.MAX_VERTEX_ATTRIBS; // Fixes https://github.com/xeokit/xeokit-sdk/issues/174 for (let ii = 0; ii < numVertexAttribs; ii++) { gl.disableVertexAttribArray(ii); } } /** * Picks an Entity. * @private */ this.pick = (function () { const tempVec3a = math.vec3(); math.mat4(); const tempMat4b = math.mat4(); const randomVec3 = math.vec3(); const up = math.vec3([0, 1, 0]); const _pickResult = new PickResult(); const nearAndFar = math.vec2(); const canvasPos = math.vec3(); const worldRayOrigin = math.vec3(); const worldRayDir = math.vec3(); math.vec3(); math.vec3(); return function (params, pickResult = _pickResult) { pickResult.reset(); updateDrawlist(); let look; let pickViewMatrix = null; let pickProjMatrix = null; let projection = null; pickResult.pickSurface = params.pickSurface; if (params.canvasPos) { canvasPos[0] = params.canvasPos[0]; canvasPos[1] = params.canvasPos[1]; pickViewMatrix = scene.camera.viewMatrix; pickProjMatrix = scene.camera.projMatrix; projection = scene.camera.projection; nearAndFar[0] = scene.camera.project.near; nearAndFar[1] = scene.camera.project.far; pickResult.canvasPos = params.canvasPos; } else { // Picking with arbitrary World-space ray // Align camera along ray and fire ray through center of canvas if (params.matrix) { pickViewMatrix = params.matrix; pickProjMatrix = scene.camera.projMatrix; projection = scene.camera.projection; nearAndFar[0] = scene.camera.project.near; nearAndFar[1] = scene.camera.project.far; } else { worldRayOrigin.set(params.origin || [0, 0, 0]); worldRayDir.set(params.direction || [0, 0, 1]); look = math.addVec3(worldRayOrigin, worldRayDir, tempVec3a); randomVec3[0] = Math.random(); randomVec3[1] = Math.random(); randomVec3[2] = Math.random(); math.normalizeVec3(randomVec3); math.cross3Vec3(worldRayDir, randomVec3, up); pickViewMatrix = math.lookAtMat4v(worldRayOrigin, look, up, tempMat4b); // pickProjMatrix = scene.camera.projMatrix; pickProjMatrix = scene.camera.ortho.matrix; projection = "ortho"; nearAndFar[0] = scene.camera.ortho.near; nearAndFar[1] = scene.camera.ortho.far; pickResult.origin = worldRayOrigin; pickResult.direction = worldRayDir; } canvasPos[0] = canvas.clientWidth * 0.5; canvasPos[1] = canvas.clientHeight * 0.5; } for (let type in drawableTypeInfo) { if (drawableTypeInfo.hasOwnProperty(type)) { const drawableList = drawableTypeInfo[type].drawableList; for (let i = 0, len = drawableList.length; i < len; i++) { const drawable = drawableList[i]; if (drawable.setPickMatrices) { // Eg. SceneModel, which needs pre-loading into texture drawable.setPickMatrices(pickViewMatrix, pickProjMatrix); } } } } const pickBuffer = renderBufferManager.getRenderBuffer("pick", {size: [1, 1]}); pickBuffer.bind(); const pickable = gpuPickPickable(pickBuffer, canvasPos, pickViewMatrix, pickProjMatrix, params, pickResult); if (!pickable) { pickBuffer.unbind(); return null; } const pickedEntity = (pickable.delegatePickedEntity) ? pickable.delegatePickedEntity() : pickable; if (!pickedEntity) { pickBuffer.unbind(); return null; } if (params.pickSurface) { // GPU-based ray-picking if (pickable.canPickTriangle && pickable.canPickTriangle()) { gpuPickTriangle(pickBuffer, pickable, canvasPos, pickViewMatrix, pickProjMatrix, pickResult); pickable.pickTriangleSurface(pickViewMatrix, pickProjMatrix, projection, pickResult); pickResult.pickSurfacePrecision = false; } else { if (pickable.canPickWorldPos && pickable.canPickWorldPos()) { gpuPickWorldPos(pickBuffer, pickable, canvasPos, pickViewMatrix, pickProjMatrix, nearAndFar, pickResult); if (params.pickSurfaceNormal !== false) { gpuPickWorldNormal(pickBuffer, pickable, canvasPos, pickViewMatrix, pickProjMatrix, pickResult); } pickResult.pickSurfacePrecision = false; } } } pickBuffer.unbind(); pickResult.entity = pickedEntity; return pickResult; }; })(); function gpuPickPickable(pickBuffer, canvasPos, pickViewMatrix, pickProjMatrix, params, pickResult) { const resolutionScale = scene.canvas.resolutionScale; frameCtx.reset(); frameCtx.backfaces = true; frameCtx.frontface = true; // "ccw" frameCtx.pickOrigin = pickResult.origin; frameCtx.pickViewMatrix = pickViewMatrix; frameCtx.pickProjMatrix = pickProjMatrix; frameCtx.pickInvisible = !!params.pickInvisible; frameCtx.pickClipPos = [ getClipPosX(canvasPos[0] * resolutionScale, gl.drawingBufferWidth), getClipPosY(canvasPos[1] * resolutionScale, gl.drawingBufferHeight) ]; gl.viewport(0, 0, 1, 1); gl.depthMask(true); gl.enable(gl.DEPTH_TEST); gl.disable(gl.CULL_FACE); gl.disable(gl.BLEND); gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); const includeEntityIds = params.includeEntityIds; const excludeEntityIds = params.excludeEntityIds; const renderDrawables = function(drawables) { for (let i = 0, len = drawables.length; i < len; i++) { const drawable = drawables[i]; if (drawable.culled === true || drawable.visible === false) { continue; } if (!drawable.drawPickMesh || (params.pickInvisible !== true && drawable.visible === false) || drawable.pickable === false) { continue; } if (includeEntityIds && !includeEntityIds[drawable.id]) { // TODO: push this logic into drawable continue; } if (excludeEntityIds && excludeEntityIds[drawable.id]) { continue; } drawable.drawPickMesh(frameCtx); } }; renderDrawables(postCullDrawableList); if (uiDrawableList.length > 0) { gl.clear(gl.DEPTH_BUFFER_BIT); renderDrawables(uiDrawableList); } const pix = pickBuffer.read(0, 0); const pickID = pix[0] + (pix[1] << 8) + (pix[2] << 16) + (pix[3] << 24); if (pickID < 0) { return; } const pickable = pickIDs.items[pickID]; return pickable; } function gpuPickTriangle(pickBuffer, pickable, canvasPos, pickViewMatrix, pickProjMatrix, pickResult) { if (!pickable.drawPickTriangles) { return; } const resolutionScale = scene.canvas.resolutionScale; frameCtx.reset(); frameCtx.backfaces = true; frameCtx.frontface = true; // "ccw" frameCtx.pickOrigin = pickResult.origin; frameCtx.pickViewMatrix = pickViewMatrix; // Can be null frameCtx.pickProjMatrix = pickProjMatrix; // Can be null // frameCtx.pickInvisible = !!params.pickInvisible; frameCtx.pickClipPos = [ getClipPosX(canvasPos[0] * resolutionScale, gl.drawingBufferWidth), getClipPosY(canvasPos[1] * resolutionScale, gl.drawingBufferHeight) ]; gl.viewport(0, 0, 1, 1); gl.clearColor(0, 0, 0, 0); gl.enable(gl.DEPTH_TEST); gl.disable(gl.CULL_FACE); gl.disable(gl.BLEND); gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); pickable.drawPickTriangles(frameCtx); const pix = pickBuffer.read(0, 0); let primIndex = pix[0] + (pix[1] * 256) + (pix[2] * 256 * 256) + (pix[3] * 256 * 256 * 256); primIndex *= 3; // Convert from triangle number to first vertex in indices pickResult.primIndex = primIndex; } const gpuPickWorldPos = (function () { const tempVec4a = math.vec4(); const tempVec4b = math.vec4(); const tempVec4c = math.vec4(); const tempVec4d = math.vec4(); const tempVec4e = math.vec4(); const tempMat4a = math.mat4(); const tempMat4b = math.mat4(); const tempMat4c = math.mat4(); return function (pickBuffer, pickable, canvasPos, pickViewMatrix, pickProjMatrix, nearAndFar, pickResult) { const resolutionScale = scene.canvas.resolutionScale; frameCtx.reset(); frameCtx.backfaces = true; frameCtx.frontface = true; // "ccw" frameCtx.pickOrigin = pickResult.origin; frameCtx.pickViewMatrix = pickViewMatrix; frameCtx.pickProjMatrix = pickProjMatrix; frameCtx.pickZNear = nearAndFar[0]; frameCtx.pickZFar = nearAndFar[1]; frameCtx.pickElementsCount = pickable.pickElementsCount; frameCtx.pickElementsOffset = pickable.pickElementsOffset; frameCtx.pickClipPos = [ getClipPosX(canvasPos[0] * resolutionScale, gl.drawingBufferWidth), getClipPosY(canvasPos[1] * resolutionScale, gl.drawingBufferHeight) ]; gl.viewport(0, 0, 1, 1); gl.clearColor(0, 0, 0, 0); gl.depthMask(true); gl.enable(gl.DEPTH_TEST); gl.disable(gl.CULL_FACE); gl.disable(gl.BLEND); gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); pickable.drawPickDepths(frameCtx); // Draw color-encoded fragment screen-space depths const pix = pickBuffer.read(0, 0); const screenZ = unpackDepth(pix); // Get screen-space Z at the given canvas coords // Calculate clip space coordinates, which will be in range of x=[-1..1] and y=[-1..1], with y=(+1) at top const x = (canvasPos[0] - canvas.clientWidth / 2) / (canvas.clientWidth / 2); const y = -(canvasPos[1] - canvas.clientHeight / 2) / (canvas.clientHeight / 2); const origin = pickable.origin; let pvMat; if (origin) { const rtcPickViewMat = createRTCViewMat(pickViewMatrix, origin, tempMat4a); pvMat = math.mulMat4(pickProjMatrix, rtcPickViewMat, tempMat4b); } else { pvMat = math.mulMat4(pickProjMatrix, pickViewMatrix, tempMat4b); } const pvMatInverse = math.inverseMat4(pvMat, tempMat4c); tempVec4a[0] = x; tempVec4a[1] = y; tempVec4a[2] = -1; tempVec4a[3] = 1; let world1 = math.transformVec4(pvMatInverse, tempVec4a); world1 = math.mulVec4Scalar(world1, 1 / world1[3]); tempVec4b[0] = x; tempVec4b[1] = y; tempVec4b[2] = 1; tempVec4b[3] = 1; let world2 = math.transformVec4(pvMatInverse, tempVec4b); world2 = math.mulVec4Scalar(world2, 1 / world2[3]); const dir = math.subVec3(world2, world1, tempVec4c); const worldPos = math.addVec3(world1, math.mulVec4Scalar(dir, screenZ, tempVec4d), tempVec4e); if (origin) { math.addVec3(worldPos, origin); } pickResult.worldPos = worldPos; } })(); function drawSnapInit(frameCtx) { frameCtx.snapPickLayerParams = []; frameCtx.snapPickLayerNumber = 0; for (let i = 0, len = postCullDrawableList.length; i < len; i++) { const drawable = postCullDrawableList[i]; if (drawable.drawSnapInit) { if (!drawable.culled && drawable.visible && drawable.pickable) { drawable.drawSnapInit(frameCtx); } } } return frameCtx.snapPickLayerParams; } function drawSnap(frameCtx) { frameCtx.snapPickLayerParams = frameCtx.snapPickLayerParams || []; frameCtx.snapPickLayerNumber = frameCtx.snapPickLayerParams.length; for (let i = 0, len = postCullDrawableList.length; i < len; i++) { const drawable = postCullDrawableList[i]; if (drawable.drawSnapInit) { if (drawable.drawSnap) { if (!drawable.culled && drawable.visible && drawable.pickable) { drawable.drawSnap(frameCtx); } } } } return frameCtx.snapPickLayerParams; } function getClipPosX(pos, size) { return 2 * (pos / size) - 1; } function getClipPosY(pos, size) { return 1 - 2 * (pos / size); } /** * @param {[number, number]} canvasPos * @param {number} [snapRadiusInPixels=30] * @param {boolean} [snapToVertex=true] * @param {boolean} [snapToEdge=true] * @param pickResult * @returns {PickResult} */ this.snapPick = (function () { const _pickResult = new PickResult(); return function (params, pickResult = _pickResult) { const {canvasPos, origin, direction, snapRadius, snapToVertex, snapToEdge} = params; if (!snapToVertex && !snapToEdge) { return this.pick({canvasPos, pickSurface: true}); } const resolutionScale = scene.canvas.resolutionScale; frameCtx.reset(); frameCtx.backfaces = true; frameCtx.frontface = true; // "ccw" frameCtx.pickZNear = scene.camera.project.near; frameCtx.pickZFar = scene.camera.project.far; const snapRadiusInPixels = snapRadius || 30; const vertexPickBuffer = renderBufferManager.getRenderBuffer( `uniquePickColors-aabs-${snapRadiusInPixels}`, { depthTexture: true, size: [ 2 * snapRadiusInPixels + 1, 2 * snapRadiusInPixels + 1, ] } ); frameCtx.snapVectorA = [ canvasPos ? getClipPosX(canvasPos[0] * resolutionScale, gl.drawingBufferWidth) : 0, canvasPos ? getClipPosY(canvasPos[1] * resolutionScale, gl.drawingBufferHeight) : 0, ]; frameCtx.snapInvVectorAB = [ gl.drawingBufferWidth / (2 * snapRadiusInPixels), gl.drawingBufferHeight / (2 * snapRadiusInPixels), ]; // Bind and clear the snap render target vertexPickBuffer.bind(gl.RGBA32I, gl.RGBA32I, gl.RGBA8UI); gl.viewport(0, 0, vertexPickBuffer.size[0], vertexPickBuffer.size[1]); gl.enable(gl.DEPTH_TEST); gl.frontFace(gl.CCW); gl.disable(gl.CULL_FACE); gl.depthMask(true); gl.disable(gl.BLEND); gl.depthFunc(gl.LEQUAL); gl.clear(gl.DEPTH_BUFFER_BIT); gl.clearBufferiv(gl.COLOR, 0, new Int32Array([0, 0, 0, 0])); gl.clearBufferiv(gl.COLOR, 1, new Int32Array([0, 0, 0, 0])); gl.clearBufferuiv(gl.COLOR, 2, new Uint32Array([0, 0, 0, 0])); ////////////////////////////////// // Set view and proj mats for VBO renderers /////////////////////////////////////// frameCtx.pickViewMatrix = (canvasPos ? scene.camera.viewMatrix : math.lookAtMat4v( origin, math.addVec3(origin, direction, math.vec3()), math.vec3([0, 1, 0]), math.mat4())); const pickProjMatrix = scene.camera.projMatrix; for (let type in drawableTypeInfo) { if (drawableTypeInfo.hasOwnProperty(type)) { const drawableList = drawableTypeInfo[type].drawableList; for (let i = 0, len = drawableList.length; i < len; i++) { const drawable = drawableList[i]; if (drawable.setPickMatrices) { // Eg. SceneModel, which needs pre-loading into texture drawable.setPickMatrices(frameCtx.pickViewMatrix, pickProjMatrix); } } } } // a) init z-buffer gl.drawBuffers([gl.COLOR_ATTACHMENT0, gl.COLOR_ATTACHMENT1, gl.COLOR_ATTACHMENT2]); const layerParamsSurface = drawSnapInit(frameCtx); // b) snap-pick const layerParamsSnap = []; frameCtx.snapPickLayerParams = layerParamsSnap; gl.depthMask(false); gl.drawBuffers([gl.COLOR_ATTACHMENT0]); if (snapToVertex && snapToEdge) { frameCtx.snapMode = "edge"; drawSnap(frameCtx); frameCtx.snapMode = "vertex"; frameCtx.snapPickLayerNumber++; drawSnap(frameCtx); } else { frameCtx.snapMode = snapToVertex ? "vertex" : "edge"; drawSnap(frameCtx); } gl.depthMask(true); // Read and decode the snapped coordinates const snapPickResultArray = vertexPickBuffer.readArray(gl.RGBA_INTEGER, gl.INT, Int32Array, 4); const snapPickNormalResultArray = vertexPickBuffer.readArray(gl.RGBA_INTEGER, gl.INT, Int32Array, 4, 1); const snapPickIdResultArray = vertexPickBuffer.readArray(gl.RGBA_INTEGER, gl.UNSIGNED_INT, Uint32Array, 4, 2); vertexPickBuffer.unbind(); // result 1) regular hi-precision world position let worldPos = null; let worldNormal = null; let pickable = null; const middleX = snapRadiusInPixels; const middleY = snapRadiusInPixels; const middleIndex = (middleX * 4) + (middleY * vertexPickBuffer.size[0] * 4); const pickResultMiddleXY = snapPickResultArray.slice(middleIndex, middleIndex + 4); const pickNormalResultMiddleXY = snapPickNormalResultArray.slice(middleIndex, middleIndex + 4); const pickPickableResultMiddleXY = snapPickIdResultArray.slice(middleIndex, middleIndex + 4); if (pickResultMiddleXY[3] !== 0) { const pickedLayerParmasSurface = layerParamsSurface[Math.abs(pickResultMiddleXY[3]) % layerParamsSurface.length]; const origin = pickedLayerParmasSurface.origin; const scale = pickedLayerParmasSurface.coordinateScale; worldPos = [ pickResultMiddleXY[0] * scale[0] + origin[0], pickResultMiddleXY[1] * scale[1] + origin[1], pickResultMiddleXY[2] * scale[2] + origin[2], ]; worldNormal = math.normalizeVec3([ pickNormalResultMiddleXY[0] / math.MAX_INT, pickNormalResultMiddleXY[1] / math.MAX_INT, pickNormalResultMiddleXY[2] / math.MAX_INT, ]); const pickID = pickPickableResultMiddleXY[0] + (pickPickableResultMiddleXY[1] << 8) + (pickPickableResultMiddleXY[2] << 16) + (pickPickableResultMiddleXY[3] << 24); pickable = pickIDs.items[pickID]; } // result 2) hi-precision snapped (to vertex/edge) world position let snapPickResult = []; for (let i = 0; i < snapPickResultArray.length; i += 4) { if (snapPickResultArray[i + 3] > 0) { const pixelNumber = Math.floor(i / 4); const w = vertexPickBuffer.size[0]; const x = pixelNumber % w - Math.floor(w / 2); const y = Math.floor(pixelNumber / w) - Math.floor(w / 2); const dist = (Math.sqrt(Math.pow(x, 2) + Math.pow(y, 2))); snapPickResult.push({ x, y, dist, isVertex: snapToVertex && snapToEdge ? snapPickResultArray[i + 3] > layerParamsSnap.length / 2 : snapToVertex, result: [ snapPickResultArray[i + 0], snapPickResultArray[i + 1], snapPickResultArray[i + 2], snapPickResultArray[i + 3], ], normal: [ snapPickNormalResultArray[i + 0], snapPickNormalResultArray[i + 1], snapPickNormalResultArray[i + 2], snapPickNormalResultArray[i + 3], ], id: [ snapPickIdResultArray[i + 0], snapPickIdResultArray[i + 1], snapPickIdResultArray[i + 2], snapPickIdResultArray[i + 3], ] }); } } let snappedWorldPos = null; let snappedWorldNormal = null; let snappedPickable = null; let snapType = null; if (snapPickResult.length > 0) { // vertex snap first, then edge snap snapPickResult.sort((a, b) => { if (a.isVertex !== b.isVertex) { return a.isVertex ? -1 : 1; } else { return a.dist - b.dist; } }); snapType = snapPickResult[0].isVertex ? "vertex" : "edge"; const snapPick = snapPickResult[0].result; const snapPickNormal = snapPickResult[0].normal; const snapPickId = snapPickResult[0].id; const pickedLayerParmas = layerParamsSnap[snapPick[3]]; const origin = pickedLayerParmas.origin; const scale = pickedLayerParmas.coordinateScale; snappedWorldNormal = math.normalizeVec3([ snapPickNormal[0] / math.MAX_INT, snapPickNormal[1] / math.MAX_INT, snapPickNormal[2] / math.MAX_INT, ]); snappedWorldPos = [ snapPick[0] * scale[0] + origin[0], snapPick[1] * scale[1] + origin[1], snapPick[2] * scale[2] + origin[2], ]; snappedPickable = pickIDs.items[ snapPickId[0] + (snapPickId[1] << 8) + (snapPickId[2] << 16) + (snapPickId[3] << 24) ]; } if (null === worldPos && null == snappedWorldPos) { // If neither regular pick or snap pick, return null return null; } let snappedCanvasPos = null; if (null !== snappedWorldPos) { snappedCanvasPos = scene.camera.projectWorldPos(snappedWorldPos); } const snappedEntity = (snappedPickable && snappedPickable.delegatePickedEntity) ? snappedPickable.delegatePickedEntity() : snappedPickable; if (!snappedEntity && pickable) { pickable = pickable.delegatePickedEntity ? pickable.delegatePickedEntity() : pickable; } pickResult.reset(); pickResult.snappedToEdge = (snapType === "edge"); pickResult.snappedToVertex = (snapType === "vertex"); pickResult.worldPos = snappedWorldPos || worldPos; pickResult.worldNormal = snappedWorldNormal || worldNormal; pickResult.entity = snappedEntity || pickable; pickResult.canvasPos = canvasPos || scene.camera.projectWorldPos(worldPos || snappedWorldPos); pickResult.snappedCanvasPos = snappedCanvasPos || canvasPos; return pickResult; }; })(); function unpackDepth(depthZ) { const vec = [depthZ[0] / 256.0, depthZ[1] / 256.0, depthZ[2] / 256.0, depthZ[3] / 256.0]; const bitShift = [1.0 / (256.0 * 256.0 * 256.0), 1.0 / (256.0 * 256.0), 1.0 / 256.0, 1.0]; return math.dotVec4(vec, bitShift); } function gpuPickWorldNormal(pickBuffer, pickable, canvasPos, pickViewMatrix, pickProjMatrix, pickResult) { const resolutionScale = scene.canvas.resolutionScale; frameCtx.reset(); frameCtx.backfaces = true; frameCtx.frontface = true; // "ccw" frameCtx.pickOrigin = pickResult.origin; frameCtx.pickViewMatrix = pickViewMatrix; frameCtx.pickProjMatrix = pickProjMatrix; frameCtx.pickClipPos = [ getClipPosX(canvasPos[0] * resolutionScale, gl.drawingBufferWidth), getClipPosY(canvasPos[1] * resolutionScale, gl.drawingBufferHeight), ]; const pickNormalBuffer = renderBufferManager.getRenderBuffer("pick-normal", {size: [3, 3]}); pickNormalBuffer.bind(gl.RGBA32I); gl.viewport(0, 0, pickNormalBuffer.size[0], pickNormalBuffer.size[1]); gl.enable(gl.DEPTH_TEST); gl.disable(gl.CULL_FACE); gl.disable(gl.BLEND); gl.clear(gl.DEPTH_BUFFER_BIT); gl.clearBufferiv(gl.COLOR, 0, new Int32Array([0, 0, 0, 0])); pickable.drawPickNormals(frameCtx); // Draw color-encoded fragment World-space normals const pix = pickNormalBuffer.read(1, 1, gl.RGBA_INTEGER, gl.INT, Int32Array, 4); pickNormalBuffer.unbind(); const worldNormal = [ pix[0] / math.MAX_INT, pix[1] / math.MAX_INT, pix[2] / math.MAX_INT, ]; math.normalizeVec3(worldNormal); pickResult.worldNormal = worldNormal; } /** * Adds a {@link Marker} for occlusion testing. * @param marker */ this.addMarker = function (marker) { this._occlusionTester = this._occlusionTester || new OcclusionTester(scene, renderBufferManager); this._occlusionTester.addMarker(marker); scene.occlusionTestCountdown = 0; }; /** * Notifies that a {@link Marker#worldPos} has updated. * @param marker */ this.markerWorldPosUpdated = function (marker) { this._occlusionTester.markerWorldPosUpdated(marker); }; /** * Removes a {@link Marker} from occlusion testing. * @param marker */ this.removeMarker = function (marker) { this._occlusionTester.removeMarker(marker); }; /** * Performs an occlusion test for all added {@link Marker}s, updating * their {@link Marker#visible} properties accordingly. */ this.doOcclusionTest = function () { if (this._occlusionTester && this._occlusionTester.needOcclusionTest) { updateDrawlist(); this._occlusionTester.bindRenderBuf(); frameCtx.reset(); frameCtx.backfaces = true; frameCtx.frontface = true; // "ccw" gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight); gl.clearColor(0, 0, 0, 0); gl.enable(gl.DEPTH_TEST); gl.disable(gl.CULL_FACE); gl.disable(gl.BLEND); gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); for (let i = 0, len = postCullDrawableList.length; i < len; i++) { const drawable = postCullDrawableList[i]; if (!drawable.drawOcclusion || drawable.culled === true || drawable.visible === false || drawable.pickable === false) { // TODO: Option to exclude transparent? continue; } drawable.drawOcclusion(frameCtx); } this._occlusionTester.drawMarkers(frameCtx); this._occlusionTester.doOcclusionTest(); // Updates Marker "visible" properties this._occlusionTester.unbindRenderBuf(); } }; /** * Read pixels from the renderer's current output. Performs a force-render first. * @param pixels * @param colors * @param len * @param opaqueOnly * @private */ this.readPixels = function (pixels, colors, len, opaqueOnly) { const snapshotBuffer = renderBufferManager.getRenderBuffer("snapshot"); snapshotBuffer.bind(); snapshotBuffer.clear(); this.render({force: true, opaqueOnly: opaqueOnly}); let color; let i; let j; let k; for (i = 0; i < len; i++) { j = i * 2; k = i * 4; color = snapshotBuffer.read(pixels[j], pixels[j + 1]); colors[k] = color[0]; colors[k + 1] = color[1]; colors[k + 2] = color[2]; colors[k + 3] = color[3]; } snapshotBuffer.unbind(); imageDirty = true; }; /** * Enter snapshot mode. * * Switches rendering to a hidden snapshot canvas. * * Exit snapshot mode using endSnapshot(). */ this.beginSnapshot = function (params = {}) { const snapshotBuffer = renderBufferManager.getRenderBuffer("snapshot"); if (params.width && params.height) { snapshotBuffer.setSize([params.width, params.height]); } snapshotBuffer.bind(); snapshotBuffer.clear(); snapshotBound = true; }; /** * When in snapshot mode, renders a frame of the current Scene state to the snapshot canvas. */ this.renderSnapshot = function () { if (!snapshotBound) { return; } const snapshotBuffer = renderBufferManager.getRenderBuffer("snapshot"); snapshotBuffer.clear(); this.render({force: true, opaqueOnly: false}); imageDirty = true; }; /** * When in snapshot mode, gets an image of the snapshot canvas. * * @private * @returns {String} The image data URI. */ this.readSnapshot = function (params) { const snapshotBuffer = renderBufferManager.getRenderBuffer("snapshot"); return snapshotBuffer.readImage(params); }; /** * Returns an HTMLCanvas containing an image of the snapshot canvas. * * - The HTMLCanvas has a CanvasRenderingContext2D. * - Expects the caller to draw more things on the HTMLCanvas (annotations etc). * * @returns {HTMLCanvasElement} */ this.readSnapshotAsCanvas = function () { const snapshotBuffer = renderBufferManager.getRenderBuffer("snapshot"); return snapshotBuffer.readImageAsCanvas(); }; /** * Exists snapshot mode. * * Switches rendering back to the main canvas. */ this.endSnapshot = function () { if (!snapshotBound) { return; } const snapshotBuffer = renderBufferManager.getRenderBuffer("snapshot"); snapshotBuffer.unbind(); snapshotBound = false; }; /** * Destroys this renderer. * @private */ this.destroy = function () { drawableTypeInfo = {}; drawables = {}; renderBufferManager.destroy(); saoOcclusionRenderer.destroy(); saoDepthLimitedBlurRenderer.destroy(); if (this._occlusionTester) { this._occlusionTester.destroy(); } }; }; /** * @desc Meditates mouse, touch and keyboard events for various interaction controls. * * Ordinarily, you would only use this component as a utility to help manage input events and state for your * own custom input handlers. * * * Located at {@link Scene#input} * * Used by (at least) {@link CameraControl} * * ## Usage * * Subscribing to mouse events on the canvas: * * ````javascript * import {Viewer} from "xeokit-sdk.es.js"; * * const viewer = new Viewer({ * canvasId: "myCanvas" * }); * * const input = viewer.scene.input; * * const onMouseDown = input.on("mousedown", (canvasCoords) => { * console.log("Mouse down at: x=" + canvasCoords[0] + ", y=" + coords[1]); * }); * * const onMouseUp = input.on("mouseup", (canvasCoords) => { * console.log("Mouse up at: x=" + canvasCoords[0] + ", y=" + canvasCoords[1]); * }); * * const onMouseClicked = input.on("mouseclicked", (canvasCoords) => { * console.log("Mouse clicked at: x=" + canvasCoords[0] + ", y=" + canvasCoords[1]); * }); * * const onDblClick = input.on("dblclick", (canvasCoords) => { * console.log("Double-click at: x=" + canvasCoords[0] + ", y=" + canvasCoords[1]); * }); * ```` * * Subscribing to keyboard events on the canvas: * * ````javascript * const onKeyDown = input.on("keydown", (keyCode) => { * switch (keyCode) { * case this.KEY_A: * console.log("The 'A' key is down"); * break; * * case this.KEY_B: * console.log("The 'B' key is down"); * break; * * case this.KEY_C: * console.log("The 'C' key is down"); * break; * * default: * console.log("Some other key is down"); * } * }); * * const onKeyUp = input.on("keyup", (keyCode) => { * switch (keyCode) { * case this.KEY_A: * console.log("The 'A' key is up"); * break; * * case this.KEY_B: * console.log("The 'B' key is up"); * break; * * case this.KEY_C: * console.log("The 'C' key is up"); * break; * * default: * console.log("Some other key is up"); * } * }); * ```` * * Checking if keys are down: * * ````javascript * const isCtrlDown = input.ctrlDown; * const isAltDown = input.altDown; * const shiftDown = input.shiftDown; * //... * * const isAKeyDown = input.keyDown[input.KEY_A]; * const isBKeyDown = input.keyDown[input.KEY_B]; * const isShiftKeyDown = input.keyDown[input.KEY_SHIFT]; * //... * * ```` * Unsubscribing from events: * * ````javascript * input.off(onMouseDown); * input.off(onMouseUp); * //... * ```` * * ## Disabling all events * * Event handling is enabled by default. * * To disable all events: * * ````javascript * myViewer.scene.input.setEnabled(false); * ```` * To enable all events again: * * ````javascript * myViewer.scene.input.setEnabled(true); * ```` * * ## Disabling keyboard input * * When the mouse is over the canvas, the canvas will consume keyboard events. Therefore, sometimes we need * to disable keyboard control, so that other UI elements can get those events. * * To disable keyboard events: * * ````javascript * myViewer.scene.input.setKeyboardEnabled(false); * ```` * * To enable keyboard events again: * * ````javascript * myViewer.scene.input.setKeyboardEnabled(true) * ```` */ class Input extends Component { /** * @private */ constructor(owner, cfg = {}) { super(owner, cfg); /** * Code for the BACKSPACE key. * @property KEY_BACKSPACE * @final * @type {Number} */ this.KEY_BACKSPACE = 8; /** * Code for the TAB key. * @property KEY_TAB * @final * @type {Number} */ this.KEY_TAB = 9; /** * Code for the ENTER key. * @property KEY_ENTER * @final * @type {Number} */ this.KEY_ENTER = 13; /** * Code for the SHIFT key. * @property KEY_SHIFT * @final * @type {Number} */ this.KEY_SHIFT = 16; /** * Code for the CTRL key. * @property KEY_CTRL * @final * @type {Number} */ this.KEY_CTRL = 17; /** * Code for the ALT key. * @property KEY_ALT * @final * @type {Number} */ this.KEY_ALT = 18; /** * Code for the PAUSE_BREAK key. * @property KEY_PAUSE_BREAK * @final * @type {Number} */ this.KEY_PAUSE_BREAK = 19; /** * Code for the CAPS_LOCK key. * @property KEY_CAPS_LOCK * @final * @type {Number} */ this.KEY_CAPS_LOCK = 20; /** * Code for the ESCAPE key. * @property KEY_ESCAPE * @final * @type {Number} */ this.KEY_ESCAPE = 27; /** * Code for the PAGE_UP key. * @property KEY_PAGE_UP * @final * @type {Number} */ this.KEY_PAGE_UP = 33; /** * Code for the PAGE_DOWN key. * @property KEY_PAGE_DOWN * @final * @type {Number} */ this.KEY_PAGE_DOWN = 34; /** * Code for the END key. * @property KEY_END * @final * @type {Number} */ this.KEY_END = 35; /** * Code for the HOME key. * @property KEY_HOME * @final * @type {Number} */ this.KEY_HOME = 36; /** * Code for the LEFT_ARROW key. * @property KEY_LEFT_ARROW * @final * @type {Number} */ this.KEY_LEFT_ARROW = 37; /** * Code for the UP_ARROW key. * @property KEY_UP_ARROW * @final * @type {Number} */ this.KEY_UP_ARROW = 38; /** * Code for the RIGHT_ARROW key. * @property KEY_RIGHT_ARROW * @final * @type {Number} */ this.KEY_RIGHT_ARROW = 39; /** * Code for the DOWN_ARROW key. * @property KEY_DOWN_ARROW * @final * @type {Number} */ this.KEY_DOWN_ARROW = 40; /** * Code for the INSERT key. * @property KEY_INSERT * @final * @type {Number} */ this.KEY_INSERT = 45; /** * Code for the DELETE key. * @property KEY_DELETE * @final * @type {Number} */ this.KEY_DELETE = 46; /** * Code for the 0 key. * @property KEY_NUM_0 * @final * @type {Number} */ this.KEY_NUM_0 = 48; /** * Code for the 1 key. * @property KEY_NUM_1 * @final * @type {Number} */ this.KEY_NUM_1 = 49; /** * Code for the 2 key. * @property KEY_NUM_2 * @final * @type {Number} */ this.KEY_NUM_2 = 50; /** * Code for the 3 key. * @property KEY_NUM_3 * @final * @type {Number} */ this.KEY_NUM_3 = 51; /** * Code for the 4 key. * @property KEY_NUM_4 * @final * @type {Number} */ this.KEY_NUM_4 = 52; /** * Code for the 5 key. * @property KEY_NUM_5 * @final * @type {Number} */ this.KEY_NUM_5 = 53; /** * Code for the 6 key. * @property KEY_NUM_6 * @final * @type {Number} */ this.KEY_NUM_6 = 54; /** * Code for the 7 key. * @property KEY_NUM_7 * @final * @type {Number} */ this.KEY_NUM_7 = 55; /** * Code for the 8 key. * @property KEY_NUM_8 * @final * @type {Number} */ this.KEY_NUM_8 = 56; /** * Code for the 9 key. * @property KEY_NUM_9 * @final * @type {Number} */ this.KEY_NUM_9 = 57; /** * Code for the A key. * @property KEY_A * @final * @type {Number} */ this.KEY_A = 65; /** * Code for the B key. * @property KEY_B * @final * @type {Number} */ this.KEY_B = 66; /** * Code for the C key. * @property KEY_C * @final * @type {Number} */ this.KEY_C = 67; /** * Code for the D key. * @property KEY_D * @final * @type {Number} */ this.KEY_D = 68; /** * Code for the E key. * @property KEY_E * @final * @type {Number} */ this.KEY_E = 69; /** * Code for the F key. * @property KEY_F * @final * @type {Number} */ this.KEY_F = 70; /** * Code for the G key. * @property KEY_G * @final * @type {Number} */ this.KEY_G = 71; /** * Code for the H key. * @property KEY_H * @final * @type {Number} */ this.KEY_H = 72; /** * Code for the I key. * @property KEY_I * @final * @type {Number} */ this.KEY_I = 73; /** * Code for the J key. * @property KEY_J * @final * @type {Number} */ this.KEY_J = 74; /** * Code for the K key. * @property KEY_K * @final * @type {Number} */ this.KEY_K = 75; /** * Code for the L key. * @property KEY_L * @final * @type {Number} */ this.KEY_L = 76; /** * Code for the M key. * @property KEY_M * @final * @type {Number} */ this.KEY_M = 77; /** * Code for the N key. * @property KEY_N * @final * @type {Number} */ this.KEY_N = 78; /** * Code for the O key. * @property KEY_O * @final * @type {Number} */ this.KEY_O = 79; /** * Code for the P key. * @property KEY_P * @final * @type {Number} */ this.KEY_P = 80; /** * Code for the Q key. * @property KEY_Q * @final * @type {Number} */ this.KEY_Q = 81; /** * Code for the R key. * @property KEY_R * @final * @type {Number} */ this.KEY_R = 82; /** * Code for the S key. * @property KEY_S * @final * @type {Number} */ this.KEY_S = 83; /** * Code for the T key. * @property KEY_T * @final * @type {Number} */ this.KEY_T = 84; /** * Code for the U key. * @property KEY_U * @final * @type {Number} */ this.KEY_U = 85; /** * Code for the V key. * @property KEY_V * @final * @type {Number} */ this.KEY_V = 86; /** * Code for the W key. * @property KEY_W * @final * @type {Number} */ this.KEY_W = 87; /** * Code for the X key. * @property KEY_X * @final * @type {Number} */ this.KEY_X = 88; /** * Code for the Y key. * @property KEY_Y * @final * @type {Number} */ this.KEY_Y = 89; /** * Code for the Z key. * @property KEY_Z * @final * @type {Number} */ this.KEY_Z = 90; /** * Code for the LEFT_WINDOW key. * @property KEY_LEFT_WINDOW * @final * @type {Number} */ this.KEY_LEFT_WINDOW = 91; /** * Code for the RIGHT_WINDOW key. * @property KEY_RIGHT_WINDOW * @final * @type {Number} */ this.KEY_RIGHT_WINDOW = 92; /** * Code for the SELECT key. * @property KEY_SELECT * @final * @type {Number} */ this.KEY_SELECT_KEY = 93; /** * Code for the number pad 0 key. * @property KEY_NUMPAD_0 * @final * @type {Number} */ this.KEY_NUMPAD_0 = 96; /** * Code for the number pad 1 key. * @property KEY_NUMPAD_1 * @final * @type {Number} */ this.KEY_NUMPAD_1 = 97; /** * Code for the number pad 2 key. * @property KEY_NUMPAD 2 * @final * @type {Number} */ this.KEY_NUMPAD_2 = 98; /** * Code for the number pad 3 key. * @property KEY_NUMPAD_3 * @final * @type {Number} */ this.KEY_NUMPAD_3 = 99; /** * Code for the number pad 4 key. * @property KEY_NUMPAD_4 * @final * @type {Number} */ this.KEY_NUMPAD_4 = 100; /** * Code for the number pad 5 key. * @property KEY_NUMPAD_5 * @final * @type {Number} */ this.KEY_NUMPAD_5 = 101; /** * Code for the number pad 6 key. * @property KEY_NUMPAD_6 * @final * @type {Number} */ this.KEY_NUMPAD_6 = 102; /** * Code for the number pad 7 key. * @property KEY_NUMPAD_7 * @final * @type {Number} */ this.KEY_NUMPAD_7 = 103; /** * Code for the number pad 8 key. * @property KEY_NUMPAD_8 * @final * @type {Number} */ this.KEY_NUMPAD_8 = 104; /** * Code for the number pad 9 key. * @property KEY_NUMPAD_9 * @final * @type {Number} */ this.KEY_NUMPAD_9 = 105; /** * Code for the MULTIPLY key. * @property KEY_MULTIPLY * @final * @type {Number} */ this.KEY_MULTIPLY = 106; /** * Code for the ADD key. * @property KEY_ADD * @final * @type {Number} */ this.KEY_ADD = 107; /** * Code for the SUBTRACT key. * @property KEY_SUBTRACT * @final * @type {Number} */ this.KEY_SUBTRACT = 109; /** * Code for the DECIMAL POINT key. * @property KEY_DECIMAL_POINT * @final * @type {Number} */ this.KEY_DECIMAL_POINT = 110; /** * Code for the DIVIDE key. * @property KEY_DIVIDE * @final * @type {Number} */ this.KEY_DIVIDE = 111; /** * Code for the F1 key. * @property KEY_F1 * @final * @type {Number} */ this.KEY_F1 = 112; /** * Code for the F2 key. * @property KEY_F2 * @final * @type {Number} */ this.KEY_F2 = 113; /** * Code for the F3 key. * @property KEY_F3 * @final * @type {Number} */ this.KEY_F3 = 114; /** * Code for the F4 key. * @property KEY_F4 * @final * @type {Number} */ this.KEY_F4 = 115; /** * Code for the F5 key. * @property KEY_F5 * @final * @type {Number} */ this.KEY_F5 = 116; /** * Code for the F6 key. * @property KEY_F6 * @final * @type {Number} */ this.KEY_F6 = 117; /** * Code for the F7 key. * @property KEY_F7 * @final * @type {Number} */ this.KEY_F7 = 118; /** * Code for the F8 key. * @property KEY_F8 * @final * @type {Number} */ this.KEY_F8 = 119; /** * Code for the F9 key. * @property KEY_F9 * @final * @type {Number} */ this.KEY_F9 = 120; /** * Code for the F10 key. * @property KEY_F10 * @final * @type {Number} */ this.KEY_F10 = 121; /** * Code for the F11 key. * @property KEY_F11 * @final * @type {Number} */ this.KEY_F11 = 122; /** * Code for the F12 key. * @property KEY_F12 * @final * @type {Number} */ this.KEY_F12 = 123; /** * Code for the NUM_LOCK key. * @property KEY_NUM_LOCK * @final * @type {Number} */ this.KEY_NUM_LOCK = 144; /** * Code for the SCROLL_LOCK key. * @property KEY_SCROLL_LOCK * @final * @type {Number} */ this.KEY_SCROLL_LOCK = 145; /** * Code for the SEMI_COLON key. * @property KEY_SEMI_COLON * @final * @type {Number} */ this.KEY_SEMI_COLON = 186; /** * Code for the EQUAL_SIGN key. * @property KEY_EQUAL_SIGN * @final * @type {Number} */ this.KEY_EQUAL_SIGN = 187; /** * Code for the COMMA key. * @property KEY_COMMA * @final * @type {Number} */ this.KEY_COMMA = 188; /** * Code for the DASH key. * @property KEY_DASH * @final * @type {Number} */ this.KEY_DASH = 189; /** * Code for the PERIOD key. * @property KEY_PERIOD * @final * @type {Number} */ this.KEY_PERIOD = 190; /** * Code for the FORWARD_SLASH key. * @property KEY_FORWARD_SLASH * @final * @type {Number} */ this.KEY_FORWARD_SLASH = 191; /** * Code for the GRAVE_ACCENT key. * @property KEY_GRAVE_ACCENT * @final * @type {Number} */ this.KEY_GRAVE_ACCENT = 192; /** * Code for the OPEN_BRACKET key. * @property KEY_OPEN_BRACKET * @final * @type {Number} */ this.KEY_OPEN_BRACKET = 219; /** * Code for the BACK_SLASH key. * @property KEY_BACK_SLASH * @final * @type {Number} */ this.KEY_BACK_SLASH = 220; /** * Code for the CLOSE_BRACKET key. * @property KEY_CLOSE_BRACKET * @final * @type {Number} */ this.KEY_CLOSE_BRACKET = 221; /** * Code for the SINGLE_QUOTE key. * @property KEY_SINGLE_QUOTE * @final * @type {Number} */ this.KEY_SINGLE_QUOTE = 222; /** * Code for the SPACE key. * @property KEY_SPACE * @final * @type {Number} */ this.KEY_SPACE = 32; /** * Code for the left mouse button. * @property MOUSE_LEFT_BUTTON * @final * @type {Number} */ this.MOUSE_LEFT_BUTTON = 260; /** * Code for the middle mouse button. * @property MOUSE_MIDDLE_BUTTON * @final * @type {Number} */ this.MOUSE_MIDDLE_BUTTON = 261; /** * Code for the right mouse button. * @property MOUSE_RIGHT_BUTTON * @final * @type {Number} */ this.MOUSE_RIGHT_BUTTON = 262; /** * The canvas element that mouse and keyboards are bound to. * * @final * @type {HTMLCanvasElement} */ this.element = cfg.element; /** True whenever ALT key is down. * * @type {boolean} */ this.altDown = false; /** True whenever CTRL key is down. * * @type {boolean} */ this.ctrlDown = false; /** True whenever left mouse button is down. * * @type {boolean} */ this.mouseDownLeft = false; /** * True whenever middle mouse button is down. * * @type {boolean} */ this.mouseDownMiddle = false; /** * True whenever the right mouse button is down. * * @type {boolean} */ this.mouseDownRight = false; /** * Flag for each key that's down. * * @type {boolean[]} */ this.keyDown = []; /** True while input enabled * * @type {boolean} */ this.enabled = true; /** True while keyboard input is enabled. * * Default value is ````true````. * * {@link CameraControl} will not respond to keyboard events while this is ````false````. * * @type {boolean} */ this.keyboardEnabled = true; /** True while the mouse is over the canvas. * * @type {boolean} */ this.mouseover = false; /** * Current mouse position within the canvas. * @type {Number[]} */ this.mouseCanvasPos = math.vec2(); this._keyboardEventsElement = cfg.keyboardEventsElement || document; this._bindEvents(); } _bindEvents() { if (this._eventsBound) { return; } this._keyboardEventsElement.addEventListener("keydown", this._keyDownListener = (e) => { if (!this.enabled || (!this.keyboardEnabled)) { return; } if (e.target.tagName !== "INPUT" && e.target.tagName !== "TEXTAREA") { if (e.keyCode === this.KEY_CTRL) { this.ctrlDown = true; } else if (e.keyCode === this.KEY_ALT) { this.altDown = true; } else if (e.keyCode === this.KEY_SHIFT) { this.shiftDown = true; } this.keyDown[e.keyCode] = true; this.fire("keydown", e.keyCode, true); } }, false); this._keyboardEventsElement.addEventListener("keyup", this._keyUpListener = (e) => { if (!this.enabled || (!this.keyboardEnabled)) { return; } if (e.target.tagName !== "INPUT" && e.target.tagName !== "TEXTAREA") { if (e.keyCode === this.KEY_CTRL) { this.ctrlDown = false; } else if (e.keyCode === this.KEY_ALT) { this.altDown = false; } else if (e.keyCode === this.KEY_SHIFT) { this.shiftDown = false; } this.keyDown[e.keyCode] = false; this.fire("keyup", e.keyCode, true); } }); this.element.addEventListener("mouseenter", this._mouseEnterListener = (e) => { if (!this.enabled) { return; } this.mouseover = true; this._getMouseCanvasPos(e); this.fire("mouseenter", this.mouseCanvasPos, true); }); this.element.addEventListener("mouseleave", this._mouseLeaveListener = (e) => { if (!this.enabled) { return; } this.mouseover = false; this._getMouseCanvasPos(e); this.fire("mouseleave", this.mouseCanvasPos, true); }); this.element.addEventListener("mousedown", this._mouseDownListener = (e) => { if (!this.enabled) { return; } switch (e.which) { case 1:// Left button this.mouseDownLeft = true; break; case 2:// Middle/both buttons this.mouseDownMiddle = true; break; case 3:// Right button this.mouseDownRight = true; break; } this._getMouseCanvasPos(e); this.element.focus(); this.fire("mousedown", this.mouseCanvasPos, true); if (this.mouseover) { e.preventDefault(); } }); document.addEventListener("mouseup", this._mouseUpListener = (e) => { if (!this.enabled) { return; } switch (e.which) { case 1:// Left button this.mouseDownLeft = false; break; case 2:// Middle/both buttons this.mouseDownMiddle = false; break; case 3:// Right button this.mouseDownRight = false; break; } this.fire("mouseup", this.mouseCanvasPos, true); // if (this.mouseover) { // e.preventDefault(); // } }, true); document.addEventListener("click", this._clickListener = (e) => { if (!this.enabled) { return; } switch (e.which) { case 1:// Left button this.mouseDownLeft = false; this.mouseDownRight = false; break; case 2:// Middle/both buttons this.mouseDownMiddle = false; break; case 3:// Right button this.mouseDownLeft = false; this.mouseDownRight = false; break; } this._getMouseCanvasPos(e); this.fire("click", this.mouseCanvasPos, true); if (this.mouseover) { e.preventDefault(); } }); document.addEventListener("dblclick", this._dblClickListener = (e) => { if (!this.enabled) { return; } switch (e.which) { case 1:// Left button this.mouseDownLeft = false; this.mouseDownRight = false; break; case 2:// Middle/both buttons this.mouseDownMiddle = false; break; case 3:// Right button this.mouseDownLeft = false; this.mouseDownRight = false; break; } this._getMouseCanvasPos(e); this.fire("dblclick", this.mouseCanvasPos, true); if (this.mouseover) { e.preventDefault(); } }); const tickifedMouseMoveFn = this.scene.tickify( () => this.fire("mousemove", this.mouseCanvasPos, true) ); this.element.addEventListener("mousemove", this._mouseMoveListener = (e) => { if (!this.enabled) { return; } this._getMouseCanvasPos(e); tickifedMouseMoveFn(); if (this.mouseover) { e.preventDefault(); } }); this.element.addEventListener("contextmenu", this._contextmenuListener = (e) => { if (!this.enabled) { return; } this._getMouseCanvasPos(e); this.fire("contextmenu", this.mouseCanvasPos, true); }); const tickifiedMouseWheelFn = this.scene.tickify( (delta) => { this.fire("mousewheel", delta, true); } ); this.element.addEventListener("wheel", this._mouseWheelListener = (e, d) => { if (!this.enabled) { return; } const delta = Math.max(-1, Math.min(1, -e.deltaY * 40)); tickifiedMouseWheelFn(delta); }, {passive: true}); // mouseclicked { let downX; let downY; // Tolerance between down and up positions for a mouse click const tolerance = 2; this.on("mousedown", (params) => { downX = params[0]; downY = params[1]; }); this.on("mouseup", (params) => { if (downX >= (params[0] - tolerance) && downX <= (params[0] + tolerance) && downY >= (params[1] - tolerance) && downY <= (params[1] + tolerance)) { this.fire("mouseclicked", params, true); } }); } this.element.addEventListener("touchstart", this._touchstartListener = (e) => { if (!this.enabled) { return; } [...e.changedTouches].forEach(e => { this.fire("touchstart", [ e.identifier, this._getTouchCanvasPos(e) ], true); }); }); this.element.addEventListener("touchend", this._touchendListener = (e) => { if (!this.enabled) { return; } [...e.changedTouches].forEach(e => { this.fire("touchend", [ e.identifier, this._getTouchCanvasPos(e) ], true); }); }); this._eventsBound = true; } _unbindEvents() { if (!this._eventsBound) { return; } this._keyboardEventsElement.removeEventListener("keydown", this._keyDownListener); this._keyboardEventsElement.removeEventListener("keyup", this._keyUpListener); this.element.removeEventListener("mouseenter", this._mouseEnterListener); this.element.removeEventListener("mouseleave", this._mouseLeaveListener); this.element.removeEventListener("mousedown", this._mouseDownListener); document.removeEventListener("mouseup", this._mouseDownListener); document.removeEventListener("click", this._clickListener); document.removeEventListener("dblclick", this._dblClickListener); this.element.removeEventListener("mousemove", this._mouseMoveListener); this.element.removeEventListener("contextmenu", this._contextmenuListener); this.element.removeEventListener("wheel", this._mouseWheelListener); this.element.removeEventListener("touchstart", this._touchstartListener); this.element.removeEventListener("touchend", this._touchendListener); if (window.OrientationChangeEvent) { window.removeEventListener('orientationchange', this._orientationchangedListener); } if (window.DeviceMotionEvent) { window.removeEventListener('devicemotion', this._deviceMotionListener); } if (window.DeviceOrientationEvent) { window.removeEventListener("deviceorientation", this._deviceOrientListener); } this._eventsBound = false; } _getTouchCanvasPos(event) { let element = event.target; let totalOffsetLeft = 0; let totalOffsetTop = 0; while (element.offsetParent) { totalOffsetLeft += element.offsetLeft; totalOffsetTop += element.offsetTop; element = element.offsetParent; } return [ event.pageX - totalOffsetLeft, event.pageY - totalOffsetTop ]; } _getMouseCanvasPos(event) { if (!event) { event = window.event; this.mouseCanvasPos[0] = event.x; this.mouseCanvasPos[1] = event.y; } else { let element = event.target; const rect = element.getBoundingClientRect(); this.mouseCanvasPos[0] = event.clientX - rect.left; this.mouseCanvasPos[1] = event.clientY - rect.top; } } /** * Sets whether input handlers are enabled. * * Default value is ````true````. * * @param {Boolean} enable Indicates if input handlers are enabled. */ setEnabled(enable) { if (this.enabled !== enable) { this.fire("enabled", this.enabled = enable); } } /** * Gets whether input handlers are enabled. * * Default value is ````true````. * * @returns {Boolean} Indicates if input handlers are enabled. */ getEnabled() { return this.enabled; } /** * Sets whether or not keyboard input is enabled. * * Default value is ````true````. * * {@link CameraControl} will not respond to keyboard events while this is set ````false````. * * @param {Boolean} value Indicates whether keyboard input is enabled. */ setKeyboardEnabled(value) { this.keyboardEnabled = value; } /** * Gets whether keyboard input is enabled. * * Default value is ````true````. * * {@link CameraControl} will not respond to keyboard events while this is set ````false````. * * @returns {Boolean} Returns whether keyboard input is enabled. */ getKeyboardEnabled() { return this.keyboardEnabled; } /** * @private */ destroy() { super.destroy(); this._unbindEvents(); } } /** * @desc controls the canvas viewport for a {@link Scene}. * * * One Viewport per scene. * * You can configure a Scene to render multiple times per frame, while setting the Viewport to different extents on each render. * * Make a Viewport automatically size to its {@link Scene} {@link Canvas} by setting its {@link Viewport#autoBoundary} ````true````. * * * Configuring the Scene to render twice on each frame, each time to a separate viewport: * * ````Javascript * // Load glTF model * var model = new xeokit.GLTFModel({ src: "models/gltf/GearboxAssy/glTF-MaterialsCommon/GearboxAssy.gltf" }); var scene = model.scene; var viewport = scene.viewport; // Configure Scene to render twice for each frame scene.passes = 2; // Default is 1 scene.clearEachPass = false; // Default is false // Render to a separate viewport on each render var viewport = scene.viewport; viewport.autoBoundary = false; scene.on("rendering", function (e) { switch (e.pass) { case 0: viewport.boundary = [0, 0, 200, 200]; // xmin, ymin, width, height break; case 1: viewport.boundary = [200, 0, 200, 200]; break; } }); ```` @class Viewport @module xeokit @submodule rendering @constructor @param {Component} owner Owner component. When destroyed, the owner will destroy this component as well. @param {*} [cfg] Viewport configuration @param {String} [cfg.id] Optional ID, unique among all components in the parent {@link Scene}, generated automatically when omitted. @param {String:Object} [cfg.meta] Optional map of user-defined metadata to attach to this Viewport. @param [cfg.boundary] {Number[]} Canvas-space Viewport boundary, given as (min, max, width, height). Defaults to the size of the parent {@link Scene} {@link Canvas}. @param [cfg.autoBoundary=false] {Boolean} Indicates if this Viewport's {@link Viewport#boundary} automatically synchronizes with the size of the parent {@link Scene} {@link Canvas}. @extends Component */ class Viewport extends Component { /** @private */ get type() { return "Viewport"; } /** @private */ constructor(owner, cfg = {}) { super(owner, cfg); this._state = new RenderState({ boundary: [0, 0, 100, 100] }); this.boundary = cfg.boundary; this.autoBoundary = cfg.autoBoundary; } /** * Sets the canvas-space boundary of this Viewport, indicated as ````[min, max, width, height]````. * * When {@link Viewport#autoBoundary} is ````true````, ignores calls to this method and automatically synchronizes with {@link Canvas#boundary}. * * Fires a "boundary"" event on change. * * Defaults to the {@link Canvas} extents. * * @param {Number[]} value New Viewport extents. */ set boundary(value) { if (this._autoBoundary) { return; } if (!value) { const canvasBoundary = this.scene.canvas.boundary; const width = canvasBoundary[2]; const height = canvasBoundary[3]; value = [0, 0, width, height]; } this._state.boundary = value; this.glRedraw(); /** Fired whenever this Viewport's {@link Viewport#boundary} property changes. @event boundary @param value {Boolean} The property's new value */ this.fire("boundary", this._state.boundary); } /** * Gets the canvas-space boundary of this Viewport, indicated as ````[min, max, width, height]````. * * @returns {Number[]} The Viewport extents. */ get boundary() { return this._state.boundary; } /** * Sets if {@link Viewport#boundary} automatically synchronizes with {@link Canvas#boundary}. * * Default is ````false````. * * @param {Boolean} value Set true to automatically sycnhronize. */ set autoBoundary(value) { value = !!value; if (value === this._autoBoundary) { return; } this._autoBoundary = value; if (this._autoBoundary) { this._onCanvasSize = this.scene.canvas.on("boundary", function (boundary) { const width = boundary[2]; const height = boundary[3]; this._state.boundary = [0, 0, width, height]; this.glRedraw(); /** Fired whenever this Viewport's {@link Viewport#boundary} property changes. @event boundary @param value {Boolean} The property's new value */ this.fire("boundary", this._state.boundary); }, this); } else if (this._onCanvasSize) { this.scene.canvas.off(this._onCanvasSize); this._onCanvasSize = null; } /** Fired whenever this Viewport's {@link autoBoundary/autoBoundary} property changes. @event autoBoundary @param value The property's new value */ this.fire("autoBoundary", this._autoBoundary); } /** * Gets if {@link Viewport#boundary} automatically synchronizes with {@link Canvas#boundary}. * * Default is ````false````. * * @returns {Boolean} Returns ````true```` when automatically sycnhronizing. */ get autoBoundary() { return this._autoBoundary; } _getState() { return this._state; } /** * @private */ destroy() { super.destroy(); this._state.destroy(); } } /** * @desc Defines its {@link Camera}'s perspective projection using a field-of-view angle. * * * Located at {@link Camera#perspective}. * * Implicitly sets the left, right, top, bottom frustum planes using {@link Perspective#fov}. * * {@link Perspective#near} and {@link Perspective#far} specify the distances to the WebGL clipping planes. */ class Perspective extends Component { /** @private */ get type() { return "Perspective"; } /** * @constructor * @private */ constructor(camera, cfg = {}) { super(camera, cfg); /** * The Camera this Perspective belongs to. * * @property camera * @type {Camera} * @final */ this.camera = camera; this._state = new RenderState({ matrix: math.mat4(), inverseMatrix: math.mat4(), transposedMatrix: math.mat4(), near: 0.1, far: 10000.0 }); this._inverseMatrixDirty = true; this._transposedMatrixDirty = true; this._fov = 60.0; // Recompute aspect from change in canvas size this._canvasResized = this.scene.canvas.on("boundary", this._needUpdate, this); this.fov = cfg.fov; this.fovAxis = cfg.fovAxis; this.near = cfg.near; this.far = cfg.far; } _update() { const WIDTH_INDEX = 2; const HEIGHT_INDEX = 3; const boundary = this.scene.canvas.boundary; const aspect = boundary[WIDTH_INDEX] / boundary[HEIGHT_INDEX]; const fovAxis = this._fovAxis; let fov = this._fov; if (fovAxis === "x" || (fovAxis === "min" && aspect < 1) || (fovAxis === "max" && aspect > 1)) { fov = fov / aspect; } fov = Math.min(fov, 120); math.perspectiveMat4(fov * (Math.PI / 180.0), aspect, this._state.near, this._state.far, this._state.matrix); this._inverseMatrixDirty = true; this._transposedMatrixDirty = true; this.glRedraw(); this.camera._updateScheduled = true; this.fire("matrix", this._state.matrix); } /** * Sets the Perspective's field-of-view angle (FOV). * * Fires an "fov" event on change. * Default value is ````60.0````. * * @param {Number} value New field-of-view. */ set fov(value) { value = (value !== undefined && value !== null) ? value : 60.0; if (value === this._fov) { return; } this._fov = value; this._needUpdate(0); // Ensure matrix built on next "tick" this.fire("fov", this._fov); } /** * Gets the Perspective's field-of-view angle (FOV). * * Default value is ````60.0````. * * @returns {Number} Current field-of-view. */ get fov() { return this._fov; } /** * Sets the Perspective's FOV axis. * * Options are ````"x"````, ````"y"```` or ````"min"````, to use the minimum axis. * * Fires an "fovAxis" event on change. * Default value ````"min"````. * * @param {String} value New FOV axis value. */ set fovAxis(value) { value = value || "min"; if (this._fovAxis === value) { return; } if (value !== "x" && value !== "y" && value !== "min") { this.error("Unsupported value for 'fovAxis': " + value + " - defaulting to 'min'"); value = "min"; } this._fovAxis = value; this._needUpdate(0); // Ensure matrix built on next "tick" this.fire("fovAxis", this._fovAxis); } /** * Gets the Perspective's FOV axis. * * Options are ````"x"````, ````"y"```` or ````"min"````, to use the minimum axis. * * Fires an "fovAxis" event on change. * Default value is ````"min"````. * * @returns {String} The current FOV axis value. */ get fovAxis() { return this._fovAxis; } /** * Sets the position of the Perspective's near plane on the positive View-space Z-axis. * * Fires a "near" event on change. * * Default value is ````0.1````. * * @param {Number} value New Perspective near plane position. */ set near(value) { const near = (value !== undefined && value !== null) ? value : 0.1; if (this._state.near === near) { return; } this._state.near = near; this._needUpdate(0); // Ensure matrix built on next "tick" this.fire("near", this._state.near); } /** * Gets the position of the Perspective's near plane on the positive View-space Z-axis. * * Fires an "emits" emits on change. * * Default value is ````0.1````. * * @returns The Perspective's near plane position. */ get near() { return this._state.near; } /** * Sets the position of this Perspective's far plane on the positive View-space Z-axis. * * Fires a "far" event on change. * * Default value is ````10000.0````. * * @param {Number} value New Perspective far plane position. */ set far(value) { const far = (value !== undefined && value !== null) ? value : 10000.0; if (this._state.far === far) { return; } this._state.far = far; this._needUpdate(0); // Ensure matrix built on next "tick" this.fire("far", this._state.far); } /** * Gets the position of this Perspective's far plane on the positive View-space Z-axis. * * Default value is ````10000.0````. * * @return {Number} The Perspective's far plane position. */ get far() { return this._state.far; } /** * Gets the Perspective's projection transform matrix. * * Fires a "matrix" event on change. * * Default value is ````[1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]````. * * @returns {Number[]} The Perspective's projection matrix. */ get matrix() { if (this._updateScheduled) { this._doUpdate(); } return this._state.matrix; } /** * Gets the inverse of {@link Perspective#matrix}. * * @returns {Number[]} The inverse of {@link Perspective#matrix}. */ get inverseMatrix() { if (this._updateScheduled) { this._doUpdate(); } if (this._inverseMatrixDirty) { math.inverseMat4(this._state.matrix, this._state.inverseMatrix); this._inverseMatrixDirty = false; } return this._state.inverseMatrix; } /** * Gets the transpose of {@link Perspective#matrix}. * * @returns {Number[]} The transpose of {@link Perspective#matrix}. */ get transposedMatrix() { if (this._updateScheduled) { this._doUpdate(); } if (this._transposedMatrixDirty) { math.transposeMat4(this._state.matrix, this._state.transposedMatrix); this._transposedMatrixDirty = false; } return this._state.transposedMatrix; } /** * Un-projects the given Canvas-space coordinates and Screen-space depth, using this Perspective projection. * * @param {Number[]} canvasPos Inputs 2D Canvas-space coordinates. * @param {Number} screenZ Inputs Screen-space Z coordinate. * @param {Number[]} screenPos Outputs 3D Screen/Clip-space coordinates. * @param {Number[]} viewPos Outputs un-projected 3D View-space coordinates. * @param {Number[]} worldPos Outputs un-projected 3D World-space coordinates. */ unproject(canvasPos, screenZ, screenPos, viewPos, worldPos) { const canvas = this.scene.canvas.canvas; const halfCanvasWidth = canvas.offsetWidth / 2.0; const halfCanvasHeight = canvas.offsetHeight / 2.0; screenPos[0] = (canvasPos[0] - halfCanvasWidth) / halfCanvasWidth; screenPos[1] = (canvasPos[1] - halfCanvasHeight) / halfCanvasHeight; screenPos[2] = screenZ; screenPos[3] = 1.0; math.mulMat4v4(this.inverseMatrix, screenPos, viewPos); math.mulVec3Scalar(viewPos, 1.0 / viewPos[3]); viewPos[3] = 1.0; viewPos[1] *= -1; math.mulMat4v4(this.camera.inverseViewMatrix, viewPos, worldPos); return worldPos; } /** @private * */ destroy() { super.destroy(); this._state.destroy(); this.scene.canvas.off(this._canvasResized); } } /** * @desc Defines its {@link Camera}'s orthographic projection as a box-shaped view volume. * * * Located at {@link Camera#ortho}. * * Works like Blender's orthographic projection, where the positions of the left, right, top and bottom planes are implicitly * indicated with a single {@link Ortho#scale} property, which causes the frustum to be symmetrical on X and Y axis, large enough to * contain the number of units given by {@link Ortho#scale}. * * {@link Ortho#near} and {@link Ortho#far} indicated the distances to the WebGL clipping planes. */ class Ortho extends Component { /** @private */ get type() { return "Ortho"; } /** * @constructor * @private */ constructor(camera, cfg = {}) { super(camera, cfg); /** * The Camera this Ortho belongs to. * * @property camera * @type {Camera} * @final */ this.camera = camera; this._state = new RenderState({ matrix: math.mat4(), inverseMatrix: math.mat4(), transposedMatrix: math.mat4(), near: 0.1, far: 10000.0 }); this._inverseMatrixDirty = true; this._transposedMatrixDirty = true; this._userNear = this._state.near; this._userFar = this._state.far; this.scale = cfg.scale; this.near = cfg.near; this.far = cfg.far; this._onCameraMatrix = this.camera.on("matrix", this._needUpdate, this); this._onCanvasBoundary = this.scene.canvas.on("boundary", this._needUpdate, this); } _update() { const WIDTH_INDEX = 2; const HEIGHT_INDEX = 3; const scene = this.scene; const scale = this._scale; const halfSize = 0.5 * scale; const userNear = (this._userNear !== undefined && this._userNear !== null) ? this._userNear : 0.1; const userFar = (this._userFar !== undefined && this._userFar !== null) ? this._userFar : 10000.0; const boundary = scene.canvas.boundary; const boundaryWidth = boundary[WIDTH_INDEX]; const boundaryHeight = boundary[HEIGHT_INDEX]; const aspect = boundaryWidth / boundaryHeight; let left; let right; let top; let bottom; if (boundaryWidth > boundaryHeight) { left = -halfSize; right = halfSize; top = halfSize / aspect; bottom = -halfSize / aspect; } else { left = -halfSize * aspect; right = halfSize * aspect; top = halfSize; bottom = -halfSize; } let near = userNear; let far = userFar; let minDepth = Infinity; let maxDepth = -Infinity; const camera = this.camera; const eye = camera._eye; const look = camera._look; const hasEyeLook = !!(eye && look && Number.isFinite(eye[0]) && Number.isFinite(eye[1]) && Number.isFinite(eye[2]) && Number.isFinite(look[0]) && Number.isFinite(look[1]) && Number.isFinite(look[2])); const aabb = scene.aabb; if (hasEyeLook && aabb && aabb[0] <= aabb[3] && Number.isFinite(aabb[0]) && Number.isFinite(aabb[3])) { const viewMatrix = camera.viewMatrix; const x0 = aabb[0]; const y0 = aabb[1]; const z0 = aabb[2]; const x1 = aabb[3]; const y1 = aabb[4]; const z1 = aabb[5]; const updateDepth = (x, y, z) => { tempVec4a$7[0] = x; tempVec4a$7[1] = y; tempVec4a$7[2] = z; tempVec4a$7[3] = 1; math.mulMat4v4(viewMatrix, tempVec4a$7, tempVec4a$7); const depth = -tempVec4a$7[2]; if (depth < minDepth) { minDepth = depth; } if (depth > maxDepth) { maxDepth = depth; } }; updateDepth(x0, y0, z0); updateDepth(x1, y0, z0); updateDepth(x0, y1, z0); updateDepth(x1, y1, z0); updateDepth(x0, y0, z1); updateDepth(x1, y0, z1); updateDepth(x0, y1, z1); updateDepth(x1, y1, z1); } const minNear = Math.max(userNear, 0.001); if (Number.isFinite(minDepth) && Number.isFinite(maxDepth)) { const margin = Math.max(1, scale * 0.5); near = Math.max(minNear, minDepth - margin); far = Math.min(userFar, maxDepth + margin); } else if (hasEyeLook) { const dist = camera.eyeLookDist; const depth = Math.max(scale * 2, 1); near = Math.max(minNear, dist - depth); far = Math.min(userFar, dist + depth); } else { near = minNear; far = Math.max(minNear + 1, userFar); } if (!Number.isFinite(near) || !Number.isFinite(far)) { near = minNear; far = Math.max(near + 1, userFar); } if (far <= near + 1e-6) { far = near + Math.max(1, scale); } this._state.near = near; this._state.far = far; math.orthoMat4c(left, right, bottom, top, near, far, this._state.matrix); this._inverseMatrixDirty = true; this._transposedMatrixDirty = true; this.glRedraw(); this.fire("matrix", this._state.matrix); } /** * Sets scale factor for this Ortho's extents on X and Y axis. * * Clamps to minimum value of ````0.01```. * * Fires a "scale" event on change. * * Default value is ````1.0```` * @param {Number} value New scale value. */ set scale(value) { if (value === undefined || value === null) { value = 1.0; } if (value <= 0) { value = 0.01; } this._scale = value; this._needUpdate(0); this.fire("scale", this._scale); } /** * Gets scale factor for this Ortho's extents on X and Y axis. * * Clamps to minimum value of ````0.01```. * * Default value is ````1.0```` * * @returns {Number} New Ortho scale value. */ get scale() { return this._scale; } /** * Sets the position of the Ortho's near plane on the positive View-space Z-axis. * * Fires a "near" emits on change. * * Default value is ````0.1````. * * @param {Number} value New Ortho near plane position. */ set near(value) { const near = (value !== undefined && value !== null) ? value : 0.1; if (this._userNear === near) { return; } this._userNear = near; this._needUpdate(0); this.fire("near", this._userNear); } /** * Gets the position of the Ortho's near plane on the positive View-space Z-axis. * * Default value is ````0.1````. * * @returns {Number} New Ortho near plane position. */ get near() { return this._state.near; } /** * Sets the position of the Ortho's far plane on the positive View-space Z-axis. * * Fires a "far" event on change. * * Default value is ````10000.0````. * * @param {Number} value New far ortho plane position. */ set far(value) { const far = (value !== undefined && value !== null) ? value : 10000.0; if (this._userFar === far) { return; } this._userFar = far; this._needUpdate(0); this.fire("far", this._userFar); } /** * Gets the position of the Ortho's far plane on the positive View-space Z-axis. * * Default value is ````10000.0````. * * @returns {Number} New far ortho plane position. */ get far() { return this._state.far; } /** * Gets the Ortho's projection transform matrix. * * Fires a "matrix" event on change. * * Default value is ````[1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]````. * * @returns {Number[]} The Ortho's projection matrix. */ get matrix() { if (this._updateScheduled) { this._doUpdate(); } return this._state.matrix; } /** * Gets the inverse of {@link Ortho#matrix}. * * @returns {Number[]} The inverse of {@link Ortho#matrix}. */ get inverseMatrix() { if (this._updateScheduled) { this._doUpdate(); } if (this._inverseMatrixDirty) { math.inverseMat4(this._state.matrix, this._state.inverseMatrix); this._inverseMatrixDirty = false; } return this._state.inverseMatrix; } /** * Gets the transpose of {@link Ortho#matrix}. * * @returns {Number[]} The transpose of {@link Ortho#matrix}. */ get transposedMatrix() { if (this._updateScheduled) { this._doUpdate(); } if (this._transposedMatrixDirty) { math.transposeMat4(this._state.matrix, this._state.transposedMatrix); this._transposedMatrixDirty = false; } return this._state.transposedMatrix; } /** * Un-projects the given Canvas-space coordinates, using this Ortho projection. * * @param {Number[]} canvasPos Inputs 2D Canvas-space coordinates. * @param {Number} screenZ Inputs Screen-space Z coordinate. * @param {Number[]} screenPos Outputs 3D Screen/Clip-space coordinates. * @param {Number[]} viewPos Outputs un-projected 3D View-space coordinates. * @param {Number[]} worldPos Outputs un-projected 3D World-space coordinates. */ unproject(canvasPos, screenZ, screenPos, viewPos, worldPos) { const canvas = this.scene.canvas.canvas; const halfCanvasWidth = canvas.offsetWidth / 2.0; const halfCanvasHeight = canvas.offsetHeight / 2.0; screenPos[0] = (canvasPos[0] - halfCanvasWidth) / halfCanvasWidth; screenPos[1] = (canvasPos[1] - halfCanvasHeight) / halfCanvasHeight; screenPos[2] = screenZ; screenPos[3] = 1.0; math.mulMat4v4(this.inverseMatrix, screenPos, viewPos); math.mulVec3Scalar(viewPos, 1.0 / viewPos[3]); viewPos[3] = 1.0; viewPos[1] *= -1; math.mulMat4v4(this.camera.inverseViewMatrix, viewPos, worldPos); return worldPos; } /** @private * */ destroy() { super.destroy(); this._state.destroy(); this.camera.off(this._onCameraMatrix); this.scene.canvas.off(this._onCanvasBoundary); } } /** * @desc Defines its {@link Camera}'s perspective projection as a frustum-shaped view volume. * * * Located at {@link Camera#frustum}. * * Allows to explicitly set the positions of the left, right, top, bottom, near and far planes, which is useful for asymmetrical view volumes, such as for stereo viewing. * * {@link Frustum#near} and {@link Frustum#far} specify the distances to the WebGL clipping planes. */ class Frustum extends Component { /** @private */ get type() { return "Frustum"; } /** * @constructor * @private */ constructor(camera, cfg = {}) { super(camera, cfg); /** * The Camera this Frustum belongs to. * * @property camera * @type {Camera} * @final */ this.camera = camera; this._state = new RenderState({ matrix: math.mat4(), inverseMatrix: math.mat4(), transposedMatrix: math.mat4(), near: 0.1, far: 10000.0 }); this._left = -1.0; this._right = 1.0; this._bottom = -1.0; this._top = 1.0; this._inverseMatrixDirty = true; this._transposedMatrixDirty = true; // Set component properties this.left = cfg.left; this.right = cfg.right; this.bottom = cfg.bottom; this.top = cfg.top; this.near = cfg.near; this.far = cfg.far; } _update() { math.frustumMat4(this._left, this._right, this._bottom, this._top, this._state.near, this._state.far, this._state.matrix); this._inverseMatrixDirty = true; this._transposedMatrixDirty = true; this.glRedraw(); this.fire("matrix", this._state.matrix); } /** * Sets the position of the Frustum's left plane on the View-space X-axis. * * Fires a {@link Frustum#left:emits} emits on change. * * @param {Number} value New left frustum plane position. */ set left(value) { this._left = (value !== undefined && value !== null) ? value : -1.0; this._needUpdate(0); this.fire("left", this._left); } /** * Gets the position of the Frustum's left plane on the View-space X-axis. * * @return {Number} Left frustum plane position. */ get left() { return this._left; } /** * Sets the position of the Frustum's right plane on the View-space X-axis. * * Fires a {@link Frustum#right:emits} emits on change. * * @param {Number} value New right frustum plane position. */ set right(value) { this._right = (value !== undefined && value !== null) ? value : 1.0; this._needUpdate(0); this.fire("right", this._right); } /** * Gets the position of the Frustum's right plane on the View-space X-axis. * * Fires a {@link Frustum#right:emits} emits on change. * * @return {Number} Right frustum plane position. */ get right() { return this._right; } /** * Sets the position of the Frustum's top plane on the View-space Y-axis. * * Fires a {@link Frustum#top:emits} emits on change. * * @param {Number} value New top frustum plane position. */ set top(value) { this._top = (value !== undefined && value !== null) ? value : 1.0; this._needUpdate(0); this.fire("top", this._top); } /** * Gets the position of the Frustum's top plane on the View-space Y-axis. * * Fires a {@link Frustum#top:emits} emits on change. * * @return {Number} Top frustum plane position. */ get top() { return this._top; } /** * Sets the position of the Frustum's bottom plane on the View-space Y-axis. * * Fires a {@link Frustum#bottom:emits} emits on change. * * @emits {"bottom"} event with the value of this property whenever it changes. * * @param {Number} value New bottom frustum plane position. */ set bottom(value) { this._bottom = (value !== undefined && value !== null) ? value : -1.0; this._needUpdate(0); this.fire("bottom", this._bottom); } /** * Gets the position of the Frustum's bottom plane on the View-space Y-axis. * * Fires a {@link Frustum#bottom:emits} emits on change. * * @return {Number} Bottom frustum plane position. */ get bottom() { return this._bottom; } /** * Sets the position of the Frustum's near plane on the positive View-space Z-axis. * * Fires a {@link Frustum#near:emits} emits on change. * * Default value is ````0.1````. * * @param {Number} value New Frustum near plane position. */ set near(value) { this._state.near = (value !== undefined && value !== null) ? value : 0.1; this._needUpdate(0); this.fire("near", this._state.near); } /** * Gets the position of the Frustum's near plane on the positive View-space Z-axis. * * Fires a {@link Frustum#near:emits} emits on change. * * Default value is ````0.1````. * * @return {Number} Near frustum plane position. */ get near() { return this._state.near; } /** * Sets the position of the Frustum's far plane on the positive View-space Z-axis. * * Fires a {@link Frustum#far:emits} emits on change. * * Default value is ````10000.0````. * * @param {Number} value New far frustum plane position. */ set far(value) { this._state.far = (value !== undefined && value !== null) ? value : 10000.0; this._needUpdate(0); this.fire("far", this._state.far); } /** * Gets the position of the Frustum's far plane on the positive View-space Z-axis. * * Default value is ````10000.0````. * * @return {Number} Far frustum plane position. */ get far() { return this._state.far; } /** * Gets the Frustum's projection transform matrix. * * Fires a {@link Frustum#matrix:emits} emits on change. * * Default value is ````[1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]````. * * @returns {Number[]} The Frustum's projection matrix matrix. */ get matrix() { if (this._updateScheduled) { this._doUpdate(); } return this._state.matrix; } /** * Gets the inverse of {@link Frustum#matrix}. * * @returns {Number[]} The inverse orthographic projection matrix. */ get inverseMatrix() { if (this._updateScheduled) { this._doUpdate(); } if (this._inverseMatrixDirty) { math.inverseMat4(this._state.matrix, this._state.inverseMatrix); this._inverseMatrixDirty = false; } return this._state.inverseMatrix; } /** * Gets the transpose of {@link Frustum#matrix}. * * @returns {Number[]} The transpose of {@link Frustum#matrix}. */ get transposedMatrix() { if (this._updateScheduled) { this._doUpdate(); } if (this._transposedMatrixDirty) { math.transposeMat4(this._state.matrix, this._state.transposedMatrix); this._transposedMatrixDirty = false; } return this._state.transposedMatrix; } /** * Un-projects the given Canvas-space coordinates, using this Frustum projection. * * @param {Number[]} canvasPos Inputs 2D Canvas-space coordinates. * @param {Number} screenZ Inputs Screen-space Z coordinate. * @param {Number[]} screenPos Outputs 3D Screen/Clip-space coordinates. * @param {Number[]} viewPos Outputs un-projected 3D View-space coordinates. * @param {Number[]} worldPos Outputs un-projected 3D World-space coordinates. */ unproject(canvasPos, screenZ, screenPos, viewPos, worldPos) { const canvas = this.scene.canvas.canvas; const halfCanvasWidth = canvas.offsetWidth / 2.0; const halfCanvasHeight = canvas.offsetHeight / 2.0; screenPos[0] = (canvasPos[0] - halfCanvasWidth) / halfCanvasWidth; screenPos[1] = (canvasPos[1] - halfCanvasHeight) / halfCanvasHeight; screenPos[2] = screenZ; screenPos[3] = 1.0; math.mulMat4v4(this.inverseMatrix, screenPos, viewPos); math.mulVec3Scalar(viewPos, 1.0 / viewPos[3]); viewPos[3] = 1.0; viewPos[1] *= -1; math.mulMat4v4(this.camera.inverseViewMatrix, viewPos, worldPos); return worldPos; } /** @private * */ destroy() { super.destroy(); this._state.destroy(); super.destroy(); } } /** * @desc Defines a custom projection for a {@link Camera} as a custom 4x4 matrix.. * * Located at {@link Camera#customProjection}. */ class CustomProjection extends Component { /** * @private */ get type() { return "CustomProjection"; } /** * @constructor * @private */ constructor(camera, cfg = {}) { super(camera, cfg); /** * The Camera this CustomProjection belongs to. * * @property camera * @type {Camera} * @final */ this.camera = camera; this._state = new RenderState({ matrix: math.mat4(), inverseMatrix: math.mat4(), transposedMatrix: math.mat4() }); this._inverseMatrixDirty = true; this._transposedMatrixDirty = false; this.matrix = cfg.matrix; } /** * Sets the CustomProjection's projection transform matrix. * * Fires a "matrix" event on change. * Default value is ````[1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]````. * * @param {Number[]} matrix New value for the CustomProjection's matrix. */ set matrix(matrix) { this._state.matrix.set(matrix || [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]); this._inverseMatrixDirty = true; this._transposedMatrixDirty = true; this.glRedraw(); this.fire("matrix", this._state.matrix); } /** * Gets the CustomProjection's projection transform matrix. * * Default value is ````[1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]````. * * @return {Number[]} New value for the CustomProjection's matrix. */ get matrix() { return this._state.matrix; } /** * Gets the inverse of {@link CustomProjection#matrix}. * * @returns {Number[]} The inverse of {@link CustomProjection#matrix}. */ get inverseMatrix() { if (this._updateScheduled) { this._doUpdate(); } if (this._inverseMatrixDirty) { math.inverseMat4(this._state.matrix, this._state.inverseMatrix); this._inverseMatrixDirty = false; } return this._state.inverseMatrix; } /** * Gets the transpose of {@link CustomProjection#matrix}. * * @returns {Number[]} The transpose of {@link CustomProjection#matrix}. */ get transposedMatrix() { if (this._updateScheduled) { this._doUpdate(); } if (this._transposedMatrixDirty) { math.transposeMat4(this._state.matrix, this._state.transposedMatrix); this._transposedMatrixDirty = false; } return this._state.transposedMatrix; } /** * Un-projects the given Canvas-space coordinates, using this CustomProjection. * * @param {Number[]} canvasPos Inputs 2D Canvas-space coordinates. * @param {Number} screenZ Inputs Screen-space Z coordinate. * @param {Number[]} screenPos Outputs 3D Screen/Clip-space coordinates. * @param {Number[]} viewPos Outputs un-projected 3D View-space coordinates. * @param {Number[]} worldPos Outputs un-projected 3D World-space coordinates. */ unproject(canvasPos, screenZ, screenPos, viewPos, worldPos) { const canvas = this.scene.canvas.canvas; const halfCanvasWidth = canvas.offsetWidth / 2.0; const halfCanvasHeight = canvas.offsetHeight / 2.0; screenPos[0] = (canvasPos[0] - halfCanvasWidth) / halfCanvasWidth; screenPos[1] = (canvasPos[1] - halfCanvasHeight) / halfCanvasHeight; screenPos[2] = screenZ; screenPos[3] = 1.0; math.mulMat4v4(this.inverseMatrix, screenPos, viewPos); math.mulVec3Scalar(viewPos, 1.0 / viewPos[3]); viewPos[3] = 1.0; viewPos[1] *= -1; math.mulMat4v4(this.camera.inverseViewMatrix, viewPos, worldPos); return worldPos; } /** @private * */ destroy() { super.destroy(); this._state.destroy(); } } const tempVec3$4 = math.vec3(); const tempVec3b$8 = math.vec3(); const tempVec3c$7 = math.vec3(); const tempVec3d$2 = math.vec3(); const tempVec3e = math.vec3(); const tempVec3f = math.vec3(); const tempVec4a$7 = math.vec4(); const tempVec4b$7 = math.vec4(); const tempVec4c$2 = math.vec4(); const tempMat = math.mat4(); const tempMatb = math.mat4(); const eyeLookVec = math.vec3(); const eyeLookVecNorm = math.vec3(); const eyeLookOffset = math.vec3(); const offsetEye = math.vec3(); /** * @desc Manages viewing and projection transforms for its {@link Scene}. * * * One Camera per {@link Scene} * * Scene is located at {@link Viewer#scene} and Camera is located at {@link Scene#camera} * * Controls viewing and projection transforms * * Has methods to pan, zoom and orbit (or first-person rotation) * * Dynamically configurable World-space axis * * Has {@link Perspective}, {@link Ortho} and {@link Frustum} and {@link CustomProjection}, which you can dynamically switch it between * * Switchable gimbal lock * * Can be "flown" to look at targets using a {@link CameraFlightAnimation} * * Can be animated along a path using a {@link CameraPathAnimation} * * ## Getting the Camera * * There is exactly one Camera per {@link Scene}: * * ````javascript * import {Viewer} from "xeokit-sdk.es.js"; * * var camera = viewer.scene.camera; * * ```` * * ## Setting the Camera Position * * Get and set the Camera's absolute position via {@link Camera#eye}, {@link Camera#look} and {@link Camera#up}: * * ````javascript * camera.eye = [-10,0,0]; * camera.look = [-10,0,0]; * camera.up = [0,1,0]; * ```` * * ## Camera View and Projection Matrices * * The Camera's view matrix transforms coordinates from World-space to View-space. * * Getting the view matrix: * * ````javascript * var viewMatrix = camera.viewMatrix; * var viewNormalMatrix = camera.normalMatrix; * ```` * * The Camera's view normal matrix transforms normal vectors from World-space to View-space. * * Getting the view normal matrix: * * ````javascript * var viewNormalMatrix = camera.normalMatrix; * ```` * * The Camera fires a ````"viewMatrix"```` event whenever the {@link Camera#viewMatrix} and {@link Camera#viewNormalMatrix} updates. * * Listen for view matrix updates: * * ````javascript * camera.on("viewMatrix", function(matrix) { ... }); * ```` * * ## Rotating the Camera * * Orbiting the {@link Camera#look} position: * * ````javascript * camera.orbitYaw(20.0); * camera.orbitPitch(10.0); * ```` * * First-person rotation, rotates {@link Camera#look} and {@link Camera#up} about {@link Camera#eye}: * * ````javascript * camera.yaw(5.0); * camera.pitch(-10.0); * ```` * * ## Panning the Camera * * Panning along the Camera's local axis (ie. left/right, up/down, forward/backward): * * ````javascript * camera.pan([-20, 0, 10]); * ```` * * ## Zooming the Camera * * Zoom to vary distance between {@link Camera#eye} and {@link Camera#look}: * * ````javascript * camera.zoom(-5); // Move five units closer * ```` * * Get the current distance between {@link Camera#eye} and {@link Camera#look}: * * ````javascript * var distance = camera.eyeLookDist; * ```` * * ## Projection * * The Camera has a Component to manage each projection type, which are: {@link Perspective}, {@link Ortho} * and {@link Frustum} and {@link CustomProjection}. * * You can configure those components at any time, regardless of which is currently active: * * The Camera has a {@link Perspective} to manage perspective * ````javascript * * // Set some properties on Perspective * camera.perspective.near = 0.4; * camera.perspective.fov = 45; * * // Set some properties on Ortho * camera.ortho.near = 0.8; * camera.ortho.far = 1000; * * // Set some properties on Frustum * camera.frustum.left = -1.0; * camera.frustum.right = 1.0; * camera.frustum.far = 1000.0; * * // Set the matrix property on CustomProjection * camera.customProjection.matrix = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]; * * // Switch between the projection types * camera.projection = "perspective"; // Switch to perspective * camera.projection = "frustum"; // Switch to frustum * camera.projection = "ortho"; // Switch to ortho * camera.projection = "customProjection"; // Switch to custom * ```` * * Camera provides the projection matrix for the currently active projection in {@link Camera#projMatrix}. * * Get the projection matrix: * * ````javascript * var projMatrix = camera.projMatrix; * ```` * * Listen for projection matrix updates: * * ````javascript * camera.on("projMatrix", function(matrix) { ... }); * ```` * * ## Configuring World up direction * * We can dynamically configure the directions of the World-space coordinate system. * * Setting the +Y axis as World "up", +X as right and -Z as forwards (convention in some modeling software): * * ````javascript * camera.worldAxis = [ * 1, 0, 0, // Right * 0, 1, 0, // Up * 0, 0,-1 // Forward * ]; * ```` * * Setting the +Z axis as World "up", +X as right and -Y as "up" (convention in most CAD and BIM viewers): * * ````javascript * camera.worldAxis = [ * 1, 0, 0, // Right * 0, 0, 1, // Up * 0,-1, 0 // Forward * ]; * ```` * * The Camera has read-only convenience properties that provide each axis individually: * * ````javascript * var worldRight = camera.worldRight; * var worldForward = camera.worldForward; * var worldUp = camera.worldUp; * ```` * * ### Gimbal locking * * By default, the Camera locks yaw rotation to pivot about the World-space "up" axis. We can dynamically lock and unlock that at any time: * * ````javascript * camera.gimbalLock = false; // Yaw rotation now happens about Camera's local Y-axis * camera.gimbalLock = true; // Yaw rotation now happens about World's "up" axis * ```` * * See: https://en.wikipedia.org/wiki/Gimbal_lock */ class Camera extends Component { /** @private */ get type() { return "Camera"; } /** * @constructor * @private */ constructor(owner, cfg = {}) { super(owner, cfg); this._state = new RenderState({ offsetEyeFromLook: true, deviceMatrix: math.mat4(), hasDeviceMatrix: false, // True when deviceMatrix set to other than identity matrix: math.mat4(), normalMatrix: math.mat4(), inverseMatrix: math.mat4() }); this._perspective = new Perspective(this); this._ortho = new Ortho(this); this._frustum = new Frustum(this); this._customProjection = new CustomProjection(this); this._project = this._perspective; this._eye = math.vec3([0, 0, 10.0]); this._look = math.vec3([0, 0, 0]); this._up = math.vec3([0, 1, 0]); this._worldUp = math.vec3([0, 1, 0]); this._worldRight = math.vec3([1, 0, 0]); this._worldForward = math.vec3([0, 0, -1]); this.deviceMatrix = cfg.deviceMatrix; this.eye = cfg.eye; this.look = cfg.look; this.up = cfg.up; this.worldAxis = cfg.worldAxis; this.gimbalLock = cfg.gimbalLock; this.constrainPitch = cfg.constrainPitch; this.projection = cfg.projection; this._perspective.on("matrix", () => { if (this._projectionType === "perspective") { this.fire("projMatrix", this._perspective.matrix); } }); this._ortho.on("matrix", () => { if (this._projectionType === "ortho") { this.fire("projMatrix", this._ortho.matrix); } }); this._frustum.on("matrix", () => { if (this._projectionType === "frustum") { this.fire("projMatrix", this._frustum.matrix); } }); this._customProjection.on("matrix", () => { if (this._projectionType === "customProjection") { this.fire("projMatrix", this._customProjection.matrix); } }); } _update() { const state = this._state; // In ortho mode, build the view matrix with an eye position that's translated // well back from look, so that the front sectionPlane plane doesn't unexpectedly cut // the front off the view (not a problem with perspective, since objects close enough // to be clipped by the front plane are usually too big to see anything of their cross-sections). let eye; if (this.projection === "ortho" && this._state.offsetEyeFromLook) { math.subVec3(this._eye, this._look, eyeLookVec); math.normalizeVec3(eyeLookVec, eyeLookVecNorm); math.mulVec3Scalar(eyeLookVecNorm, 1000.0, eyeLookOffset); math.addVec3(this._look, eyeLookOffset, offsetEye); eye = offsetEye; } else { eye = this._eye; } if (state.hasDeviceMatrix) { math.lookAtMat4v(eye, this._look, this._up, tempMatb); math.mulMat4(state.deviceMatrix, tempMatb, state.matrix); //state.matrix.set(state.deviceMatrix); } else { math.lookAtMat4v(eye, this._look, this._up, state.matrix); } math.inverseMat4(this._state.matrix, this._state.inverseMatrix); math.transposeMat4(this._state.inverseMatrix, this._state.normalMatrix); this.glRedraw(); this.fire("matrix", this._state.matrix); this.fire("viewMatrix", this._state.matrix); } /** * Rotates {@link Camera#eye} about {@link Camera#look}, around the {@link Camera#up} vector * * @param {Number} angleInc Angle of rotation in degrees */ orbitYaw(angleInc) { let lookEyeVec = math.subVec3(this._eye, this._look, tempVec3$4); math.rotationMat4v(angleInc * 0.0174532925, this._gimbalLock ? this._worldUp : this._up, tempMat); lookEyeVec = math.transformPoint3(tempMat, lookEyeVec, tempVec3b$8); this.eye = math.addVec3(this._look, lookEyeVec, tempVec3c$7); // Set eye position as 'look' plus 'eye' vector this.up = math.transformPoint3(tempMat, this._up, tempVec3d$2); // Rotate 'up' vector } /** * Rotates {@link Camera#eye} about {@link Camera#look} around the right axis (orthogonal to {@link Camera#up} and "look"). * * @param {Number} angleInc Angle of rotation in degrees */ orbitPitch(angleInc) { if (this._constrainPitch) { angleInc = math.dotVec3(this._up, this._worldUp) / math.DEGTORAD; if (angleInc < 1) { return; } } let eye2 = math.subVec3(this._eye, this._look, tempVec3$4); const left = math.cross3Vec3(math.normalizeVec3(eye2, tempVec3b$8), math.normalizeVec3(this._up, tempVec3c$7)); math.rotationMat4v(angleInc * 0.0174532925, left, tempMat); eye2 = math.transformPoint3(tempMat, eye2, tempVec3d$2); this.up = math.transformPoint3(tempMat, this._up, tempVec3e); this.eye = math.addVec3(eye2, this._look, tempVec3f); } /** * Rotates {@link Camera#look} about {@link Camera#eye}, around the {@link Camera#up} vector. * * @param {Number} angleInc Angle of rotation in degrees */ yaw(angleInc) { let look2 = math.subVec3(this._look, this._eye, tempVec3$4); math.rotationMat4v(angleInc * 0.0174532925, this._gimbalLock ? this._worldUp : this._up, tempMat); look2 = math.transformPoint3(tempMat, look2, tempVec3b$8); this.look = math.addVec3(look2, this._eye, tempVec3c$7); if (this._gimbalLock) { this.up = math.transformPoint3(tempMat, this._up, tempVec3d$2); } } /** * Rotates {@link Camera#look} about {@link Camera#eye}, around the right axis (orthogonal to {@link Camera#up} and "look"). * @param {Number} angleInc Angle of rotation in degrees */ pitch(angleInc) { if (this._constrainPitch) { angleInc = math.dotVec3(this._up, this._worldUp) / math.DEGTORAD; if (angleInc < 1) { return; } } let look2 = math.subVec3(this._look, this._eye, tempVec3$4); const left = math.cross3Vec3(math.normalizeVec3(look2, tempVec3b$8), math.normalizeVec3(this._up, tempVec3c$7)); math.rotationMat4v(angleInc * 0.0174532925, left, tempMat); this.up = math.transformPoint3(tempMat, this._up, tempVec3f); look2 = math.transformPoint3(tempMat, look2, tempVec3d$2); this.look = math.addVec3(look2, this._eye, tempVec3e); } /** * Pans the Camera along its local X, Y and Z axis. * * @param pan The pan vector */ pan(pan) { const eye2 = math.subVec3(this._eye, this._look, tempVec3$4); const vec = [0, 0, 0]; let v; if (pan[0] !== 0) { const left = math.cross3Vec3(math.normalizeVec3(eye2, []), math.normalizeVec3(this._up, tempVec3b$8)); v = math.mulVec3Scalar(left, pan[0]); vec[0] += v[0]; vec[1] += v[1]; vec[2] += v[2]; } if (pan[1] !== 0) { v = math.mulVec3Scalar(math.normalizeVec3(this._up, tempVec3c$7), pan[1]); vec[0] += v[0]; vec[1] += v[1]; vec[2] += v[2]; } if (pan[2] !== 0) { v = math.mulVec3Scalar(math.normalizeVec3(eye2, tempVec3d$2), pan[2]); vec[0] += v[0]; vec[1] += v[1]; vec[2] += v[2]; } this.eye = math.addVec3(this._eye, vec, tempVec3e); this.look = math.addVec3(this._look, vec, tempVec3f); } /** * Increments/decrements the Camera's zoom factor, which is the distance between {@link Camera#eye} and {@link Camera#look}. * * @param {Number} delta Zoom factor increment. */ zoom(delta) { const vec = math.subVec3(this._eye, this._look, tempVec3$4); const lenLook = Math.abs(math.lenVec3(vec, tempVec3b$8)); const newLenLook = Math.abs(lenLook + delta); if (newLenLook < 0.5) { return; } const dir = math.normalizeVec3(vec, tempVec3c$7); this.eye = math.addVec3(this._look, math.mulVec3Scalar(dir, newLenLook), tempVec3d$2); } /** * Sets the position of the Camera's eye. * * Default value is ````[0,0,10]````. * * @emits "eye" event on change, with the value of this property. * @type {Number[]} New eye position. */ set eye(eye) { this._eye.set(eye || [0, 0, 10]); this._needUpdate(0); // Ensure matrix built on next "tick" this.fire("eye", this._eye); } /** * Gets the position of the Camera's eye. * * Default vale is ````[0,0,10]````. * * @type {Number[]} New eye position. */ get eye() { return this._eye; } /** * Sets the position of this Camera's point-of-interest. * * Default value is ````[0,0,0]````. * * @emits "look" event on change, with the value of this property. * * @param {Number[]} look Camera look position. */ set look(look) { this._look.set(look || [0, 0, 0]); this._needUpdate(0); // Ensure matrix built on next "tick" this.fire("look", this._look); } /** * Gets the position of this Camera's point-of-interest. * * Default value is ````[0,0,0]````. * * @returns {Number[]} Camera look position. */ get look() { return this._look; } /** * Sets the direction of this Camera's {@link Camera#up} vector. * * @emits "up" event on change, with the value of this property. * * @param {Number[]} up Direction of "up". */ set up(up) { this._up.set(up || [0, 1, 0]); this._needUpdate(0); this.fire("up", this._up); } /** * Gets the direction of this Camera's {@link Camera#up} vector. * * @returns {Number[]} Direction of "up". */ get up() { return this._up; } /** * Sets an optional matrix to premultiply into {@link Camera#matrix} matrix. * * This is intended to be used for stereo rendering with WebVR etc. * * @param {Number[]} matrix The matrix. */ set deviceMatrix(matrix) { this._state.deviceMatrix.set(matrix || [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]); this._state.hasDeviceMatrix = !!matrix; this._needUpdate(0); this.fire("deviceMatrix", this._state.deviceMatrix); } /** * Gets an optional matrix to premultiply into {@link Camera#matrix} matrix. * * @returns {Number[]} The matrix. */ get deviceMatrix() { return this._state.deviceMatrix; } /** * Sets the up, right and forward axis of the World coordinate system. * * Has format: ````[rightX, rightY, rightZ, upX, upY, upZ, forwardX, forwardY, forwardZ]```` * * Default axis is ````[1, 0, 0, 0, 1, 0, 0, 0, 1]```` * * @param {Number[]} axis The new Wworld coordinate axis. */ set worldAxis(axis) { axis = axis || [1, 0, 0, 0, 1, 0, 0, 0, 1]; if (!this._worldAxis) { this._worldAxis = math.vec3(axis); } else { this._worldAxis.set(axis); } this._worldRight[0] = this._worldAxis[0]; this._worldRight[1] = this._worldAxis[1]; this._worldRight[2] = this._worldAxis[2]; this._worldUp[0] = this._worldAxis[3]; this._worldUp[1] = this._worldAxis[4]; this._worldUp[2] = this._worldAxis[5]; this._worldForward[0] = this._worldAxis[6]; this._worldForward[1] = this._worldAxis[7]; this._worldForward[2] = this._worldAxis[8]; this.fire("worldAxis", this._worldAxis); } /** * Gets the up, right and forward axis of the World coordinate system. * * Has format: ````[rightX, rightY, rightZ, upX, upY, upZ, forwardX, forwardY, forwardZ]```` * * Default axis is ````[1, 0, 0, 0, 1, 0, 0, 0, 1]```` * * @returns {Number[]} The current World coordinate axis. */ get worldAxis() { return this._worldAxis; } /** * Gets the direction of World-space "up". * * This is set by {@link Camera#worldAxis}. * * Default value is ````[0,1,0]````. * * @returns {Number[]} The "up" vector. */ get worldUp() { return this._worldUp; } /** * Gets if the World-space X-axis is "up". * @returns {Boolean} */ get xUp() { return this._worldUp[0] > this._worldUp[1] && this._worldUp[0] > this._worldUp[2]; } /** * Gets if the World-space Y-axis is "up". * @returns {Boolean} */ get yUp() { return this._worldUp[1] > this._worldUp[0] && this._worldUp[1] > this._worldUp[2]; } /** * Gets if the World-space Z-axis is "up". * @returns {Boolean} */ get zUp() { return this._worldUp[2] > this._worldUp[0] && this._worldUp[2] > this._worldUp[1]; } /** * Gets the direction of World-space "right". * * This is set by {@link Camera#worldAxis}. * * Default value is ````[1,0,0]````. * * @returns {Number[]} The "up" vector. */ get worldRight() { return this._worldRight; } /** * Gets the direction of World-space "forwards". * * This is set by {@link Camera#worldAxis}. * * Default value is ````[0,0,1]````. * * @returns {Number[]} The "up" vector. */ get worldForward() { return this._worldForward; } /** * Sets whether to lock yaw rotation to pivot about the World-space "up" axis. * * Fires a {@link Camera#gimbalLock:event} event on change. * * @params {Boolean} gimbalLock Set true to lock gimbal. */ set gimbalLock(value) { this._gimbalLock = value !== false; this.fire("gimbalLock", this._gimbalLock); } /** * Gets whether to lock yaw rotation to pivot about the World-space "up" axis. * * @returns {Boolean} Returns ````true```` if gimbal is locked. */ get gimbalLock() { return this._gimbalLock; } /** * Sets whether to prevent camera from being pitched upside down. * * The camera is upside down when the angle between {@link Camera#up} and {@link Camera#worldUp} is less than one degree. * * Fires a {@link Camera#constrainPitch:event} event on change. * * Default value is ````false````. * * @param {Boolean} value Set ````true```` to contrain pitch rotation. */ set constrainPitch(value) { this._constrainPitch = !!value; this.fire("constrainPitch", this._constrainPitch); } /** * Gets whether to prevent camera from being pitched upside down. * * The camera is upside down when the angle between {@link Camera#up} and {@link Camera#worldUp} is less than one degree. * * Default value is ````false````. * * @returns {Boolean} ````true```` if pitch rotation is currently constrained. get constrainPitch() { return this._constrainPitch; } /** * Gets distance from {@link Camera#look} to {@link Camera#eye}. * * @returns {Number} The distance. */ get eyeLookDist() { return math.lenVec3(math.subVec3(this._look, this._eye, tempVec3$4)); } /** * Gets the Camera's viewing transformation matrix. * * Fires a {@link Camera#matrix:event} event on change. * * @returns {Number[]} The viewing transform matrix. */ get matrix() { if (this._updateScheduled) { this._doUpdate(); } return this._state.matrix; } /** * Gets the Camera's viewing transformation matrix. * * Fires a {@link Camera#matrix:event} event on change. * * @returns {Number[]} The viewing transform matrix. */ get viewMatrix() { if (this._updateScheduled) { this._doUpdate(); } return this._state.matrix; } /** * The Camera's viewing normal transformation matrix. * * Fires a {@link Camera#matrix:event} event on change. * * @returns {Number[]} The viewing normal transform matrix. */ get normalMatrix() { if (this._updateScheduled) { this._doUpdate(); } return this._state.normalMatrix; } /** * The Camera's viewing normal transformation matrix. * * Fires a {@link Camera#matrix:event} event on change. * * @returns {Number[]} The viewing normal transform matrix. */ get viewNormalMatrix() { if (this._updateScheduled) { this._doUpdate(); } return this._state.normalMatrix; } /** * Gets the inverse of the Camera's viewing transform matrix. * * This has the same value as {@link Camera#normalMatrix}. * * @returns {Number[]} The inverse viewing transform matrix. */ get inverseViewMatrix() { if (this._updateScheduled) { this._doUpdate(); } return this._state.inverseMatrix; } /** * Gets the Camera's projection transformation projMatrix. * * Fires a {@link Camera#projMatrix:event} event on change. * * @returns {Number[]} The projection matrix. */ get projMatrix() { return this[this.projection].matrix; } /** * Gets the Camera's perspective projection. * * The Camera uses this while {@link Camera#projection} equals ````perspective````. * * @returns {Perspective} The Perspective component. */ get perspective() { return this._perspective; } /** * Gets the Camera's orthographic projection. * * The Camera uses this while {@link Camera#projection} equals ````ortho````. * * @returns {Ortho} The Ortho component. */ get ortho() { return this._ortho; } /** * Gets the Camera's frustum projection. * * The Camera uses this while {@link Camera#projection} equals ````frustum````. * * @returns {Frustum} The Ortho component. */ get frustum() { return this._frustum; } /** * Gets the Camera's custom projection. * * This is used while {@link Camera#projection} equals "customProjection". * * @returns {CustomProjection} The custom projection. */ get customProjection() { return this._customProjection; } /** * Sets the active projection type. * * Accepted values are ````"perspective"````, ````"ortho"````, ````"frustum"```` and ````"customProjection"````. * * Default value is ````"perspective"````. * * @param {String} value Identifies the active projection type. */ set projection(value) { value = value || "perspective"; if (this._projectionType === value) { return; } if (value === "perspective") { this._project = this._perspective; } else if (value === "ortho") { this._project = this._ortho; } else if (value === "frustum") { this._project = this._frustum; } else if (value === "customProjection") { this._project = this._customProjection; } else { this.error("Unsupported value for 'projection': " + value + " defaulting to 'perspective'"); this._project = this._perspective; value = "perspective"; } this._project._update(); this._projectionType = value; this.glRedraw(); this._update(); // Need to rebuild lookat matrix with full eye, look & up this.fire("dirty"); this.fire("projection", this._projectionType); this.fire("projMatrix", this._project.matrix); } /** * Gets the active projection type. * * Possible values are ````"perspective"````, ````"ortho"````, ````"frustum"```` and ````"customProjection"````. * * Default value is ````"perspective"````. * * @returns {String} Identifies the active projection type. */ get projection() { return this._projectionType; } /** * Gets the currently active projection for this Camera. * * The currently active project is selected with {@link Camera#projection}. * * @returns {Perspective|Ortho|Frustum|CustomProjection} The currently active projection is active. */ get project() { return this._project; } /** * Gets whether to offset the ortho camera's effective eye from its look * * Default value for legacy reasons is ````true````. */ get offsetEyeFromLook() { return this._state.offsetEyeFromLook; } /** * Offsets the ortho camera's effective eye from its look * * Default value for legacy reasons is ````true````. */ set offsetEyeFromLook(v) { this._state.offsetEyeFromLook = (v !== false); this._needUpdate(0); // Ensure matrix built on next "tick" } /** * Get the 2D canvas position of a 3D world position. * * @param {[number, number, number]} worldPos * @returns {[number, number]} the canvas position */ projectWorldPos(worldPos) { const _worldPos = tempVec4a$7; const viewPos = tempVec4b$7; const screenPos = tempVec4c$2; _worldPos[0] = worldPos[0]; _worldPos[1] = worldPos[1]; _worldPos[2] = worldPos[2]; _worldPos[3] = 1; math.mulMat4v4(this.viewMatrix, _worldPos, viewPos); math.mulMat4v4(this.projMatrix, viewPos, screenPos); math.mulVec3Scalar(screenPos, 1.0 / screenPos[3]); screenPos[3] = 1.0; screenPos[1] *= -1; const canvas = this.scene.canvas.canvas; const halfCanvasWidth = canvas.offsetWidth / 2.0; const halfCanvasHeight = canvas.offsetHeight / 2.0; const canvasPos = [ screenPos[0] * halfCanvasWidth + halfCanvasWidth, screenPos[1] * halfCanvasHeight + halfCanvasHeight, ]; return canvasPos; } /** * Destroys this Camera. */ destroy() { super.destroy(); this._state.destroy(); } } //---------------------------------------------------------------------------------------------------------------------- const unitsInfo = { meters: { abbrev: "m" }, metres: { abbrev: "m" }, centimeters: { abbrev: "cm" }, centimetres: { abbrev: "cm" }, millimeters: { abbrev: "mm" }, millimetres: { abbrev: "mm" }, yards: { abbrev: "yd" }, feet: { abbrev: "ft" }, inches: { abbrev: "in" } }; /** * @desc Configures its {@link Scene}'s measurement unit and mapping between the Real-space and World-space 3D Cartesian coordinate systems. * * * ## Overview * * * Located at {@link Scene#metrics}. * * {@link Metrics#units} configures the Real-space unit type, which is ````"meters"```` by default. * * {@link Metrics#scale} configures the number of Real-space units represented by each unit within the World-space 3D coordinate system. This is ````1.0```` by default. * * {@link Metrics#origin} configures the 3D Real-space origin, in current Real-space units, at which this {@link Scene}'s World-space coordinate origin sits, This is ````[0,0,0]```` by default. * * ## Usage * * Let's load a model using an {@link XKTLoaderPlugin}, then configure the Real-space unit type and the coordinate * mapping between the Real-space and World-space 3D coordinate systems. * * ````JavaScript * import {Viewer, XKTLoaderPlugin} from "xeokit-sdk.es.js"; * * const viewer = new Viewer({ * canvasId: "myCanvas" * }); * * viewer.scene.camera.eye = [-2.37, 18.97, -26.12]; * viewer.scene.camera.look = [10.97, 5.82, -11.22]; * viewer.scene.camera.up = [0.36, 0.83, 0.40]; * * const xktLoader = new XKTLoaderPlugin(viewer); * * const model = xktLoader.load({ * src: "./models/xkt/duplex/duplex.xkt" * }); * * const metrics = viewer.scene.metrics; * * metrics.units = "meters"; * metrics.scale = 10.0; * metrics.origin = [100.0, 0.0, 200.0]; * ```` */ class Metrics extends Component { /** * @constructor * @private */ constructor(owner, cfg = {}) { super(owner, cfg); this._units = "meters"; this._scale = 1.0; this._origin = math.vec3([0, 0, 0]); this.units = cfg.units; this.scale = cfg.scale; this.origin = cfg.origin; } /** * Gets info about the supported Real-space unit types. * * This will be: * * ````javascript * { * { * meters: { * abbrev: "m" * }, * metres: { * abbrev: "m" * }, * centimeters: { * abbrev: "cm" * }, * centimetres: { * abbrev: "cm" * }, * millimeters: { * abbrev: "mm" * }, * millimetres: { * abbrev: "mm" * }, * yards: { * abbrev: "yd" * }, * feet: { * abbrev: "ft" * }, * inches: { * abbrev: "in" * } * } * } * ```` * * @type {*} */ get unitsInfo() { return unitsInfo; } /** * Sets the {@link Scene}'s Real-space unit type. * * Accepted values are ````"meters"````, ````"centimeters"````, ````"millimeters"````, ````"metres"````, ````"centimetres"````, ````"millimetres"````, ````"yards"````, ````"feet"```` and ````"inches"````. * * @emits ````"units"```` event on change, with the value of this property. * @type {String} */ set units(value) { if (!value) { value = "meters"; } const info = unitsInfo[value]; if (!info) { this.error("Unsupported value for 'units': " + value + " defaulting to 'meters'"); value = "meters"; } this._units = value; this.fire("units", this._units); } /** * Gets the {@link Scene}'s Real-space unit type. * * @type {String} */ get units() { return this._units; } /** * Sets the number of Real-space units represented by each unit of the {@link Scene}'s World-space coordinate system. * * For example, if {@link Metrics#units} is ````"meters"````, and there are ten meters per World-space coordinate system unit, then ````scale```` would have a value of ````10.0````. * * @emits ````"scale"```` event on change, with the value of this property. * @type {Number} */ set scale(value) { value = value || 1; if (value <= 0) { this.error("scale value should be larger than zero"); return; } this._scale = value; this.fire("scale", this._scale); } /** * Gets the number of Real-space units represented by each unit of the {@link Scene}'s World-space coordinate system. * * @type {Number} */ get scale() { return this._scale; } /** * Sets the Real-space 3D origin, in Real-space units, at which this {@link Scene}'s World-space coordinate origin ````[0,0,0]```` sits. * * @emits "origin" event on change, with the value of this property. * @type {Number[]} */ set origin(value) { if (!value) { this._origin[0] = 0; this._origin[1] = 0; this._origin[2] = 0; return; } this._origin[0] = value[0]; this._origin[1] = value[1]; this._origin[2] = value[2]; this.fire("origin", this._origin); } /** * Gets the 3D Real-space origin, in Real-space units, at which this {@link Scene}'s World-space coordinate origin ````[0,0,0]```` sits. * * @type {Number[]} */ get origin() { return this._origin; } /** * Converts a 3D position from World-space to Real-space. * * This is equivalent to ````realPos = #origin + (worldPos * #scale)````. * * @param {Number[]} worldPos World-space 3D position, in World coordinate system units. * @param {Number[]} [realPos] Destination for Real-space 3D position. * @returns {Number[]} Real-space 3D position, in units indicated by {@link Metrics#units}. */ worldToRealPos(worldPos, realPos = math.vec3(3)) { realPos[0] = this._origin[0] + (this._scale * worldPos[0]); realPos[1] = this._origin[1] + (this._scale * worldPos[1]); realPos[2] = this._origin[2] + (this._scale * worldPos[2]); } /** * Converts a 3D position from Real-space to World-space. * * This is equivalent to ````worldPos = (worldPos - #origin) / #scale````. * * @param {Number[]} realPos Real-space 3D position. * @param {Number[]} [worldPos] Destination for World-space 3D position. * @returns {Number[]} World-space 3D position. */ realToWorldPos(realPos, worldPos = math.vec3(3)) { worldPos[0] = (realPos[0] - this._origin[0]) / this._scale; worldPos[1] = (realPos[1] - this._origin[1]) / this._scale; worldPos[2] = (realPos[2] - this._origin[2]) / this._scale; return worldPos; } } /** * @desc Configures Scalable Ambient Obscurance (SAO) for a {@link Scene}. * * * * [[Run this example](https://xeokit.github.io/xeokit-sdk/examples/viewer/#sao_ConferenceCenter)] * * ## Overview * * SAO approximates [Ambient Occlusion](https://en.wikipedia.org/wiki/Ambient_occlusion) in realtime. It darkens creases, cavities and surfaces * that are close to each other, which tend to be occluded from ambient light and appear darker. * * The animated GIF above shows the effect as we repeatedly enable and disable SAO. When SAO is enabled, we can see darkening * in regions such as the corners, and the crevices between stairs. This increases the amount of detail we can see when ambient * light is high, or when objects have uniform colors across their surfaces. Run the example to experiment with the various * SAO configurations. * * xeokit's implementation of SAO is based on the paper [Scalable Ambient Obscurance](https://research.nvidia.com/sites/default/files/pubs/2012-06_Scalable-Ambient-Obscurance/McGuire12SAO.pdf). * * ## Caveats * * Currently, SAO only works with perspective and orthographic projections. Therefore, to use SAO, make sure {@link Camera#projection} is * either "perspective" or "ortho". * * {@link SAO#scale} and {@link SAO#intensity} must be tuned to the distance * between {@link Perspective#near} and {@link Perspective#far}, or the distance * between {@link Ortho#near} and {@link Ortho#far}, depending on which of those two projections the {@link Camera} is currently * using. Use the [live example](https://xeokit.github.io/xeokit-sdk/examples/viewer/#sao_ConferenceCenter) to get a * feel for that. * * ## Usage * * In the example below, we'll start by logging a warning message to the console if SAO is not supported by the * system. * *Then we'll enable and configure SAO, position the camera, and configure the near and far perspective and orthographic * clipping planes. Finally, we'll use {@link XKTLoaderPlugin} to load the OTC Conference Center model. * * ````javascript * import {Viewer, XKTLoaderPlugin} from "xeokit-sdk.es.js"; * * const viewer = new Viewer({ * canvasId: "myCanvas", * transparent: true * }); * * const sao = viewer.scene.sao; * * if (!sao.supported) { * sao.warn("SAO is not supported on this system - ignoring SAO configs") * } * * sao.enabled = true; // Enable SAO - only works if supported (see above) * sao.intensity = 0.15; * sao.bias = 0.5; * sao.scale = 1.0; * sao.minResolution = 0.0; * sao.numSamples = 10; * sao.kernelRadius = 100; * sao.blendCutoff = 0.1; * * const camera = viewer.scene.camera; * * camera.eye = [3.69, 5.83, -23.98]; * camera.look = [84.31, -29.88, -116.21]; * camera.up = [0.18, 0.96, -0.21]; * * camera.perspective.near = 0.1; * camera.perspective.far = 2000.0; * * camera.ortho.near = 0.1; * camera.ortho.far = 2000.0; * camera.projection = "perspective"; * * const xktLoader = new XKTLoaderPlugin(viewer); * * const model = xktLoader.load({ * id: "myModel", * src: "./models/xkt/OTCConferenceCenter.xkt" * edges: true * }); * ```` * * [[Run this example](https://xeokit.github.io/xeokit-sdk/examples/viewer/#sao_ConferenceCenter)] * * ## Efficiency * * SAO can incur some rendering overhead, especially on objects that are viewed close to the camera. For this reason, * it's recommended to use a low value for {@link SAO#kernelRadius}. A low radius will sample pixels that are close * to the source pixel, which will allow the GPU to efficiently cache those pixels. When {@link Camera#projection} is "perspective", * objects near to the viewpoint will use larger radii than farther pixels. Therefore, computing SAO for close objects * is more expensive than for objects far away, that occupy fewer pixels on the canvas. * * ## Selectively enabling SAO for models * * When loading multiple models into a Scene, we sometimes only want SAO on the models that are actually going to * show it, such as the architecture or structure, and not show SAO on models that won't show it well, such as the * electrical wiring, or plumbing. * * To illustrate, lets load some of the models for the West Riverside Hospital. We'll enable SAO on the structure model, * but disable it on the electrical and plumbing. * * This will only apply SAO to those models if {@link SAO#supported} and {@link SAO#enabled} are both true. * * Note, by the way, how we load the models in sequence. Since XKTLoaderPlugin uses scratch memory as part of its loading * process, this allows the plugin to reuse that same memory across multiple loads, instead of having to create multiple * pools of scratch memory. * * ````javascript * const structure = xktLoader.load({ * id: "structure", * src: "./models/xkt/WestRiverSideHospital/structure.xkt" * edges: true, * saoEnabled: true * }); * * structure.on("loaded", () => { * * const electrical = xktLoader.load({ * id: "electrical", * src: "./models/xkt/WestRiverSideHospital/electrical.xkt", * edges: true * }); * * electrical.on("loaded", () => { * * const plumbing = xktLoader.load({ * id: "plumbing", * src: "./models/xkt/WestRiverSideHospital/plumbing.xkt", * edges: true * }); * }); * }); * ```` * * ## Disabling SAO while camera is moving * * For smoother interaction with large models on low-power hardware, we can disable SAO while the {@link Camera} is moving: * * ````javascript * const timeoutDuration = 150; // Milliseconds * var timer = timeoutDuration; * var saoDisabled = false; * * const onCameraMatrix = scene.camera.on("matrix", () => { * timer = timeoutDuration; * if (!saoDisabled) { * scene.sao.enabled = false; * saoDisabled = true; * } * }); * * const onSceneTick = scene.on("tick", (tickEvent) => { * if (!saoDisabled) { * return; * } * timer -= tickEvent.deltaTime; // Milliseconds * if (timer <= 0) { * if (saoDisabled) { * scene.sao.enabled = true; * saoDisabled = false; * } * } * }); * ```` * * [[Run this example](https://xeokit.github.io/xeokit-sdk/examples/index.html#techniques_nonInteractiveQuality)] */ class SAO extends Component { /** @private */ constructor(owner, cfg = {}) { super(owner, cfg); this._supported = WEBGL_INFO.SUPPORTED_EXTENSIONS["OES_standard_derivatives"]; // For computing normals in SAO fragment shader this.enabled = cfg.enabled; this.kernelRadius = cfg.kernelRadius; this.intensity = cfg.intensity; this.bias = cfg.bias; this.scale = cfg.scale; this.minResolution = cfg.minResolution; this.numSamples = cfg.numSamples; this.blur = cfg.blur; this.blendCutoff = cfg.blendCutoff; this.blendFactor = cfg.blendFactor; } /** * Gets whether or not SAO is supported by this browser and GPU. * * Even when enabled, SAO will only work if supported. * * @type {Boolean} */ get supported() { return this._supported; } /** * Sets whether SAO is enabled for the {@link Scene}. * * Even when enabled, SAO will only work if supported. * * Default value is ````false````. * * @type {Boolean} */ set enabled(value) { value = !!value; if (this._enabled === value) { return; } this._enabled = value; this.glRedraw(); } /** * Gets whether SAO is enabled for the {@link Scene}. * * Even when enabled, SAO will only apply if supported. * * Default value is ````false````. * * @type {Boolean} */ get enabled() { return this._enabled; } /** * Returns true if SAO is currently possible, where it is supported, enabled, and the current scene state is compatible. * Called internally by renderer logic. * @private * @returns {Boolean} */ get possible() { if (!this._supported) { return false; } if (!this._enabled) { return false; } const projection = this.scene.camera.projection; if (projection === "customProjection") { return false; } if (projection === "frustum") { return false; } return true; } /** * @private * @returns {boolean|*} */ get active() { return this._active; } /** * Sets the maximum area that SAO takes into account when checking for possible occlusion for each fragment. * * Default value is ````100.0````. * * @type {Number} */ set kernelRadius(value) { if (value === undefined || value === null) { value = 100.0; } if (this._kernelRadius === value) { return; } this._kernelRadius = value; this.glRedraw(); } /** * Gets the maximum area that SAO takes into account when checking for possible occlusion for each fragment. * * Default value is ````100.0````. * * @type {Number} */ get kernelRadius() { return this._kernelRadius; } /** * Sets the degree of darkening (ambient obscurance) produced by the SAO effect. * * Default value is ````0.15````. * * @type {Number} */ set intensity(value) { if (value === undefined || value === null) { value = 0.15; } if (this._intensity === value) { return; } this._intensity = value; this.glRedraw(); } /** * Gets the degree of darkening (ambient obscurance) produced by the SAO effect. * * Default value is ````0.15````. * * @type {Number} */ get intensity() { return this._intensity; } /** * Sets the SAO bias. * * Default value is ````0.5````. * * @type {Number} */ set bias(value) { if (value === undefined || value === null) { value = 0.5; } if (this._bias === value) { return; } this._bias = value; this.glRedraw(); } /** * Gets the SAO bias. * * Default value is ````0.5````. * * @type {Number} */ get bias() { return this._bias; } /** * Sets the SAO occlusion scale. * * Default value is ````1.0````. * * @type {Number} */ set scale(value) { if (value === undefined || value === null) { value = 1.0; } if (this._scale === value) { return; } this._scale = value; this.glRedraw(); } /** * Gets the SAO occlusion scale. * * Default value is ````1.0````. * * @type {Number} */ get scale() { return this._scale; } /** * Sets the SAO minimum resolution. * * Default value is ````0.0````. * * @type {Number} */ set minResolution(value) { if (value === undefined || value === null) { value = 0.0; } if (this._minResolution === value) { return; } this._minResolution = value; this.glRedraw(); } /** * Gets the SAO minimum resolution. * * Default value is ````0.0````. * * @type {Number} */ get minResolution() { return this._minResolution; } /** * Sets the number of SAO samples. * * Default value is ````10````. * * Update this sparingly, since it causes a shader recompile. * * @type {Number} */ set numSamples(value) { if (value === undefined || value === null) { value = 10; } if (this._numSamples === value) { return; } this._numSamples = value; this.glRedraw(); } /** * Gets the number of SAO samples. * * Default value is ````10````. * * @type {Number} */ get numSamples() { return this._numSamples; } /** * Sets whether Guassian blur is enabled. * * Default value is ````true````. * * @type {Boolean} */ set blur(value) { value = (value !== false); if (this._blur === value) { return; } this._blur = value; this.glRedraw(); } /** * Gets whether Guassian blur is enabled. * * Default value is ````true````. * * @type {Boolean} */ get blur() { return this._blur; } /** * Sets the SAO blend cutoff. * * Default value is ````0.3````. * * Normally you don't need to alter this. * * @type {Number} */ set blendCutoff(value) { if (value === undefined || value === null) { value = 0.3; } if (this._blendCutoff === value) { return; } this._blendCutoff = value; this.glRedraw(); } /** * Gets the SAO blend cutoff. * * Default value is ````0.3````. * * Normally you don't need to alter this. * * @type {Number} */ get blendCutoff() { return this._blendCutoff; } /** * Sets the SAO blend factor. * * Default value is ````1.0````. * * Normally you don't need to alter this. * * @type {Number} */ set blendFactor(value) { if (value === undefined || value === null) { value = 1.0; } if (this._blendFactor === value) { return; } this._blendFactor = value; this.glRedraw(); } /** * Gets the SAO blend scale. * * Default value is ````1.0````. * * Normally you don't need to alter this. * * @type {Number} */ get blendFactor() { return this._blendFactor; } /** * Destroys this component. */ destroy() { super.destroy(); } } /** * @desc Configures cross-section slices for a {@link Scene}. * * ## Overview * * Cross-sections allow to create an additional colored slice for used clipping planes. It is only a visual effect * calculated by shaders. It makes it easier to see the intersection between the model and the clipping plane this way. * * ## Usage * * In the example below, we'll configure CrossSections to manipulate the slice representation. * * ````javascript * //------------------------------------------------------------------------------------------------------------------ * // Import the modules we need for this example * //------------------------------------------------------------------------------------------------------------------ * * import {PhongMaterial, Viewer, math, SectionPlanesPlugin, XKTLoaderPlugin, Mesh, ReadableGeometry, buildPolylineGeometryFromCurve, SplineCurve} from "../../dist/xeokit-sdk.es.js"; * * //------------------------------------------------------------------------------------------------------------------ * // Create a Viewer and arrange the camera * //------------------------------------------------------------------------------------------------------------------ * * const viewer = new Viewer({ * canvasId: "myCanvas", * transparent: true * }); * * viewer.camera.eye = [-2.341298674548419, 22.43987089731119, 7.236688436028655]; * viewer.camera.look = [4.399999999999963, 3.7240000000000606, 8.899000000000006]; * viewer.camera.up = [0.9102954845584759, 0.34781746407929504, 0.22446635042673466]; * * const cameraControl = viewer.cameraControl; * cameraControl.navMode = "orbit"; * cameraControl.followPointer = true; * * //---------------------------------------------------------------------------------------------------------------------- * // Create a xeokit loader plugin, load a model, fit to view * //---------------------------------------------------------------------------------------------------------------------- * * const xktLoader = new XKTLoaderPlugin(viewer); * * var t0 = performance.now(); * * document.getElementById("time").innerHTML = "Loading model..."; * * const sceneModel = xktLoader.load({ * id: "myModel", * src: "../../assets/models/xkt/v10/glTF-Embedded/Duplex_A_20110505.glTFEmbedded.xkt", * edges: true * }); * * sceneModel.on("loaded", () => { * var t1 = performance.now(); * document.getElementById("time").innerHTML = "Model loaded in " + Math.floor(t1 - t0) / 1000.0 + " seconds
Objects: " + sceneModel.numEntities; * * let path = new SplineCurve(viewer.scene, { * points: [ * [0, 0, -10], * [0, 0, -3], * [10, 0, 10], * [10, 0, 30], * ], * }); * * new Mesh(viewer.scene, { * geometry: new ReadableGeometry(viewer.scene, buildPolylineGeometryFromCurve({ * id: "SplineCurve", * curve: path, * divisions: 50, * })), * material: new PhongMaterial(viewer.scene, { * emissive: [1, 0, 0] * }) * }); * * //------------------------------------------------------------------------------------------------------------------ * // Create a moving SectionPlane, that moves through the table models * //------------------------------------------------------------------------------------------------------------------ * * const sectionPlanes = new SectionPlanesPlugin(viewer, { * overviewCanvasId: "mySectionPlanesOverviewCanvas", * overviewVisible: true * }); * * let currentPoint = path.getPoint(0); * let currentDirection = path.getTangent(0); * * const sectionPlane = sectionPlanes.createSectionPlane({ * id: "mySectionPlane", * pos: currentPoint, * dir: currentDirection * }); * * sectionPlanes.showControl(sectionPlane.id); * * //------------------------------------------------------------------------------------------------------------------ * // Controlling SectionPlane position and direction * //------------------------------------------------------------------------------------------------------------------ * * let currentT = 0.0; * document.getElementById("section_path").oninput = function() { * currentT = Number(document.getElementById("section_path").value); * currentPoint = path.getPoint(currentT); * currentDirection = path.getTangent(currentT); * sectionPlane.pos = currentPoint; * sectionPlane.dir = currentDirection; * }; * * window.viewer = viewer; * * //------------------------------------------------------------------------------------------------------------------ * // Controlling CrossSections settings * //------------------------------------------------------------------------------------------------------------------ * * viewer.scene.crossSections.sliceThickness = 0.05; * viewer.scene.crossSections.sliceColor = [0.0, 0.0, 0.0, 1.0]; * }); * ```` * * [[Run this example](https://xeokit.github.io/xeokit-sdk/examples/slicing/#SectionPlanesPlugin_Duplex_SectionPath_CrossSections)] * */ class CrossSections extends Component { /** @private */ constructor(owner, cfg = {}) { super(owner, cfg); this.sliceColor = cfg.sliceColor; this.sliceThickness = cfg.sliceThickness; } /** * Sets the thickness of a slice created by a section. * * Default value is ````0.0````. * * @type {Number} */ set sliceThickness(value) { if (value === undefined || value === null) { value = 0.0; } if (this._sliceThickness === value) { return; } this._sliceThickness = value; this.glRedraw(); } /** * Gets the thickness of a slice created by a section. * * Default value is ````0.0````. * * @type {Number} */ get sliceThickness() { return this._sliceThickness; } /** * Sets the color of a slice created by a section. * * Default value is ````[0.0, 0.0, 0.0, 1.0]````. * * @type {Number} */ set sliceColor(value) { if (value === undefined || value === null) { value = [0.0, 0.0, 0.0, 1.0]; } if (this._sliceColor === value) { return; } this._sliceColor = value; this.glRedraw(); } /** * Gets the color of a slice created by a section. * * Default value is ````[0.0, 0.0, 0.0, 1.0]````. * * @type {Number} */ get sliceColor() { return this._sliceColor; } /** * Destroys this component. */ destroy() { super.destroy(); } } const PRESETS$1 = { "default": { pointSize: 4, roundPoints: true, perspectivePoints: true }, "square": { pointSize: 4, roundPoints: false, perspectivePoints: true }, "round": { pointSize: 4, roundPoints: true, perspectivePoints: true } }; /** * @desc Configures the size and shape of "points" geometry primitives. * * * Located at {@link Scene#pointsMaterial}. * * Supports round and square points. * * Optional perspective point scaling. * * Globally configures "points" primitives for all {@link VBOSceneModel}s. * * ## Usage * * In the example below, we'll customize the {@link Scene}'s global ````PointsMaterial````, then use * an {@link XKTLoaderPlugin} to load a model containing a point cloud. * * [[Run this example](/examples/index.html#materials_PointsMaterial)] * * ````javascript * import {Viewer, XKTLoaderPlugin} from "xeokit-sdk.es.js"; * * const viewer = new Viewer({ * canvasId: "myCanvas", * transparent: true * }); * * viewer.scene.camera.eye = [0, 0, 5]; * viewer.scene.camera.look = [0, 0, 0]; * viewer.scene.camera.up = [0, 1, 0]; * * viewer.scene.pointsMaterial.pointSize = 2; * viewer.scene.pointsMaterial.roundPoints = true; * viewer.scene.pointsMaterial.perspectivePoints = true; * viewer.scene.pointsMaterial.minPerspectivePointSize = 1; * viewer.scene.pointsMaterial.maxPerspectivePointSize = 6; * viewer.scene.pointsMaterial.filterIntensity = true; * viewer.scene.pointsMaterial.minIntensity = 0.0; * viewer.scene.pointsMaterial.maxIntensity = 1.0; * * const xktLoader = new XKTLoaderPlugin(viewer); * * const model = xktLoader.load({ * id: "myModel", * src: "../assets/models/xkt/MAP-PointCloud.xkt" * }); * ```` */ class PointsMaterial extends Material { /** @private */ get type() { return "PointsMaterial"; } /** * Gets available PointsMaterial presets. * * @type {Object} */ get presets() { return PRESETS$1; }; /** * @constructor * @param {Component} owner Owner component. When destroyed, the owner will destroy this component as well. * @param {*} [cfg] The PointsMaterial configuration * @param {String} [cfg.id] Optional ID, unique among all components in the parent {@link Scene}, generated automatically when omitted. * @param {Number} [cfg.pointSize=2] Point size in pixels. * @param {Boolean} [cfg.roundPoints=true] Whether points are round (````true````) or square (````false````). * @param {Boolean} [cfg.perspectivePoints=true] Whether apparent point size reduces with distance when {@link Camera#projection} is set to "perspective". * @param {Number} [cfg.minPerspectivePointSize=1] When ````perspectivePoints```` is ````true````, this is the minimum rendered size of each point in pixels. * @param {Number} [cfg.maxPerspectivePointSize=6] When ````perspectivePoints```` is ````true````, this is the maximum rendered size of each point in pixels. * @param {Boolean} [cfg.filterIntensity=false] When this is true, points are only rendered when their intensity value falls within the range given in {@link } * @param {Number} [cfg.minIntensity=0] When ````filterIntensity```` is ````true````, points with intensity below this value will not be rendered. * @param {Number} [cfg.maxIntensity=1] When ````filterIntensity```` is ````true````, points with intensity above this value will not be rendered. * @param {String} [cfg.preset] Selects a preset PointsMaterial configuration - see {@link PointsMaterial#presets}. */ constructor(owner, cfg = {}) { super(owner, cfg); this._state = new RenderState({ type: "PointsMaterial", pointSize: null, roundPoints: null, perspectivePoints: null, minPerspectivePointSize: null, maxPerspectivePointSize: null, filterIntensity: null, minIntensity: null, maxIntensity: null }); if (cfg.preset) { // Apply preset then override with configs where provided this.preset = cfg.preset; if (cfg.pointSize !== undefined) { this.pointSize = cfg.pointSize; } if (cfg.roundPoints !== undefined) { this.roundPoints = cfg.roundPoints; } if (cfg.perspectivePoints !== undefined) { this.perspectivePoints = cfg.perspectivePoints; } if (cfg.minPerspectivePointSize !== undefined) { this.minPerspectivePointSize = cfg.minPerspectivePointSize; } if (cfg.maxPerspectivePointSize !== undefined) { this.maxPerspectivePointSize = cfg.minPerspectivePointSize; } } else { this._preset = "default"; this.pointSize = cfg.pointSize; this.roundPoints = cfg.roundPoints; this.perspectivePoints = cfg.perspectivePoints; this.minPerspectivePointSize = cfg.minPerspectivePointSize; this.maxPerspectivePointSize = cfg.maxPerspectivePointSize; } this.filterIntensity = cfg.filterIntensity; this.minIntensity = cfg.minIntensity; this.maxIntensity = cfg.maxIntensity; } /** * Sets point size. * * Default value is ````2.0```` pixels. * * @type {Number} */ set pointSize(value) { this._state.pointSize = value || 2.0; this.glRedraw(); } /** * Gets point size. * * Default value is ````2.0```` pixels. * * @type {Number} */ get pointSize() { return this._state.pointSize; } /** * Sets if points are round or square. * * Default is ````true```` to set points round. * * @type {Boolean} */ set roundPoints(value) { value = (value !== false); if (this._state.roundPoints === value) { return; } this._state.roundPoints = value; this.scene._needRecompile = true; this.glRedraw(); } /** * Gets if points are round or square. * * Default is ````true```` to set points round. * * @type {Boolean} */ get roundPoints() { return this._state.roundPoints; } /** * Sets if rendered point size reduces with distance when {@link Camera#projection} is set to ````"perspective"````. * * Default is ````true````. * * @type {Boolean} */ set perspectivePoints(value) { value = (value !== false); if (this._state.perspectivePoints === value) { return; } this._state.perspectivePoints = value; this.scene._needRecompile = true; this.glRedraw(); } /** * Gets if rendered point size reduces with distance when {@link Camera#projection} is set to "perspective". * * Default is ````false````. * * @type {Boolean} */ get perspectivePoints() { return this._state.perspectivePoints; } /** * Sets the minimum rendered size of points when {@link PointsMaterial#perspectivePoints} is ````true````. * * Default value is ````1.0```` pixels. * * @type {Number} */ set minPerspectivePointSize(value) { this._state.minPerspectivePointSize = value || 1.0; this.scene._needRecompile = true; this.glRedraw(); } /** * Gets the minimum rendered size of points when {@link PointsMaterial#perspectivePoints} is ````true````. * * Default value is ````1.0```` pixels. * * @type {Number} */ get minPerspectivePointSize() { return this._state.minPerspectivePointSize; } /** * Sets the maximum rendered size of points when {@link PointsMaterial#perspectivePoints} is ````true````. * * Default value is ````6```` pixels. * * @type {Number} */ set maxPerspectivePointSize(value) { this._state.maxPerspectivePointSize = value || 6; this.scene._needRecompile = true; this.glRedraw(); } /** * Gets the maximum rendered size of points when {@link PointsMaterial#perspectivePoints} is ````true````. * * Default value is ````6```` pixels. * * @type {Number} */ get maxPerspectivePointSize() { return this._state.maxPerspectivePointSize; } /** * Sets if rendered point size reduces with distance when {@link Camera#projection} is set to ````"perspective"````. * * Default is ````false````. * * @type {Boolean} */ set filterIntensity(value) { value = (value !== false); if (this._state.filterIntensity === value) { return; } this._state.filterIntensity = value; this.scene._needRecompile = true; this.glRedraw(); } /** * Gets if rendered point size reduces with distance when {@link Camera#projection} is set to "perspective". * * Default is ````false````. * * @type {Boolean} */ get filterIntensity() { return this._state.filterIntensity; } /** * Sets the minimum rendered size of points when {@link PointsMaterial#perspectivePoints} is ````true````. * * Default value is ````0````. * * @type {Number} */ set minIntensity(value) { this._state.minIntensity = (value !== undefined && value !== null) ? value: 0.0; this.glRedraw(); } /** * Gets the minimum rendered size of points when {@link PointsMaterial#filterIntensity} is ````true````. * * Default value is ````0````. * * @type {Number} */ get minIntensity() { return this._state.minIntensity; } /** * Sets the maximum rendered size of points when {@link PointsMaterial#filterIntensity} is ````true````. * * Default value is ````1````. * * @type {Number} */ set maxIntensity(value) { this._state.maxIntensity = (value !== undefined && value !== null) ? value: 1.0; this.glRedraw(); } /** * Gets the maximum rendered size of points when {@link PointsMaterial#filterIntensity} is ````true````. * * Default value is ````1````. * * @type {Number} */ get maxIntensity() { return this._state.maxIntensity; } /** * Selects a preset ````PointsMaterial```` configuration. * * Default value is ````"default"````. * * @type {String} */ set preset(value) { value = value || "default"; if (this._preset === value) { return; } const preset = PRESETS$1[value]; if (!preset) { this.error("unsupported preset: '" + value + "' - supported values are " + Object.keys(PRESETS$1).join(", ")); return; } this.pointSize = preset.pointSize; this.roundPoints = preset.roundPoints; this.perspectivePoints = preset.perspectivePoints; this.minPerspectivePointSize = preset.minPerspectivePointSize; this.maxPerspectivePointSize = preset.maxPerspectivePointSize; this._preset = value; } /** * The current preset ````PointsMaterial```` configuration. * * Default value is ````"default"````. * * @type {String} */ get preset() { return this._preset; } /** * @private * @return {string} */ get hash() { return [ this.pointSize, this.roundPoints, this.perspectivePoints, this.minPerspectivePointSize, this.maxPerspectivePointSize, this.filterIntensity ].join((";")); } /** * Destroys this ````PointsMaterial````. */ destroy() { super.destroy(); this._state.destroy(); } } const PRESETS = { "default": { lineWidth: 1 }, "thick": { lineWidth: 2 }, "thicker": { lineWidth: 4 } }; /** * @desc Configures the shape of "lines" geometry primitives. * * * Located at {@link Scene#linesMaterial}. * * Globally configures "lines" primitives for all {@link VBOSceneModel}s. * * ## Usage * * In the example below, we'll customize the {@link Scene}'s global ````LinesMaterial````, then use * an {@link XKTLoaderPlugin} to load a model containing line segments. * * [[Run this example](/examples/index.html#materials_LinesMaterial)] * * ````javascript * import {Viewer, XKTLoaderPlugin} from "xeokit-sdk.es.js"; * * const viewer = new Viewer({ * canvasId: "myCanvas", * transparent: true * }); * * viewer.scene.camera.eye = [0, 0, 5]; * viewer.scene.camera.look = [0, 0, 0]; * viewer.scene.camera.up = [0, 1, 0]; * * viewer.scene.linesMaterial.lineWidth = 3; * * const xktLoader = new XKTLoaderPlugin(viewer); * * const model = xktLoader.load({ * id: "myModel", * src: "./models/xkt/Duplex.ifc.xkt" * }); * ```` */ class LinesMaterial extends Material { /** @private */ get type() { return "LinesMaterial"; } /** * Gets available LinesMaterial presets. * * @type {Object} */ get presets() { return PRESETS; }; /** * @constructor * @param {Component} owner Owner component. When destroyed, the owner will destroy this component as well. * @param {*} [cfg] The LinesMaterial configuration * @param {String} [cfg.id] Optional ID, unique among all components in the parent {@link Scene}, generated automatically when omitted. * @param {Number} [cfg.lineWidth=1] Line width in pixels. * @param {String} [cfg.preset] Selects a preset LinesMaterial configuration - see {@link LinesMaterial#presets}. */ constructor(owner, cfg = {}) { super(owner, cfg); this._state = new RenderState({ type: "LinesMaterial", lineWidth: null }); if (cfg.preset) { // Apply preset then override with configs where provided this.preset = cfg.preset; if (cfg.lineWidth !== undefined) { this.lineWidth = cfg.lineWidth; } } else { this._preset = "default"; this.lineWidth = cfg.lineWidth; } } /** * Sets line width. * * Default value is ````1```` pixels. * * @type {Number} */ set lineWidth(value) { this._state.lineWidth = value || 1; this.glRedraw(); } /** * Gets the line width. * * Default value is ````1```` pixels. * * @type {Number} */ get lineWidth() { return this._state.lineWidth; } /** * Selects a preset LinesMaterial configuration. * * Default value is ````"default"````. * * @type {String} */ set preset(value) { value = value || "default"; if (this._preset === value) { return; } const preset = PRESETS[value]; if (!preset) { this.error("unsupported preset: '" + value + "' - supported values are " + Object.keys(PRESETS).join(", ")); return; } this.lineWidth = preset.lineWidth; this._preset = value; } /** * The current preset LinesMaterial configuration. * * Default value is ````"default"````. * * @type {String} */ get preset() { return this._preset; } /** * @private * @return {string} */ get hash() { return ["" + this.lineWidth].join((";")); } /** * Destroys this LinesMaterial. */ destroy() { super.destroy(); this._state.destroy(); } } function earcut(data, holeIndices, dim = 2) { const hasHoles = holeIndices && holeIndices.length; const outerLen = hasHoles ? holeIndices[0] * dim : data.length; let outerNode = linkedList(data, 0, outerLen, dim, true); const triangles = []; if (!outerNode || outerNode.next === outerNode.prev) return triangles; let minX, minY, invSize; if (hasHoles) outerNode = eliminateHoles(data, holeIndices, outerNode, dim); // if the shape is not too simple, we'll use z-order curve hash later; calculate polygon bbox if (data.length > 80 * dim) { minX = Infinity; minY = Infinity; let maxX = -Infinity; let maxY = -Infinity; for (let i = dim; i < outerLen; i += dim) { const x = data[i]; const y = data[i + 1]; if (x < minX) minX = x; if (y < minY) minY = y; if (x > maxX) maxX = x; if (y > maxY) maxY = y; } // minX, minY and invSize are later used to transform coords into integers for z-order calculation invSize = Math.max(maxX - minX, maxY - minY); invSize = invSize !== 0 ? 32767 / invSize : 0; } earcutLinked(outerNode, triangles, dim, minX, minY, invSize, 0); return triangles; } // create a circular doubly linked list from polygon points in the specified winding order function linkedList(data, start, end, dim, clockwise) { let last; if (clockwise === (signedArea(data, start, end, dim) > 0)) { for (let i = start; i < end; i += dim) last = insertNode(i / dim | 0, data[i], data[i + 1], last); } else { for (let i = end - dim; i >= start; i -= dim) last = insertNode(i / dim | 0, data[i], data[i + 1], last); } if (last && equals$1(last, last.next)) { removeNode(last); last = last.next; } return last; } // eliminate colinear or duplicate points function filterPoints(start, end) { if (!start) return start; if (!end) end = start; let p = start, again; do { again = false; if (!p.steiner && (equals$1(p, p.next) || area(p.prev, p, p.next) === 0)) { removeNode(p); p = end = p.prev; if (p === p.next) break; again = true; } else { p = p.next; } } while (again || p !== end); return end; } // main ear slicing loop which triangulates a polygon (given as a linked list) function earcutLinked(ear, triangles, dim, minX, minY, invSize, pass) { if (!ear) return; // interlink polygon nodes in z-order if (!pass && invSize) indexCurve(ear, minX, minY, invSize); let stop = ear; // iterate through ears, slicing them one by one while (ear.prev !== ear.next) { const prev = ear.prev; const next = ear.next; if (invSize ? isEarHashed(ear, minX, minY, invSize) : isEar(ear)) { triangles.push(prev.i, ear.i, next.i); // cut off the triangle removeNode(ear); // skipping the next vertex leads to less sliver triangles ear = next.next; stop = next.next; continue; } ear = next; // if we looped through the whole remaining polygon and can't find any more ears if (ear === stop) { // try filtering points and slicing again if (!pass) { earcutLinked(filterPoints(ear), triangles, dim, minX, minY, invSize, 1); // if this didn't work, try curing all small self-intersections locally } else if (pass === 1) { ear = cureLocalIntersections(filterPoints(ear), triangles); earcutLinked(ear, triangles, dim, minX, minY, invSize, 2); // as a last resort, try splitting the remaining polygon into two } else if (pass === 2) { splitEarcut(ear, triangles, dim, minX, minY, invSize); } break; } } } // check whether a polygon node forms a valid ear with adjacent nodes function isEar(ear) { const a = ear.prev, b = ear, c = ear.next; if (area(a, b, c) >= 0) return false; // reflex, can't be an ear // now make sure we don't have other points inside the potential ear const ax = a.x, bx = b.x, cx = c.x, ay = a.y, by = b.y, cy = c.y; // triangle bbox const x0 = Math.min(ax, bx, cx), y0 = Math.min(ay, by, cy), x1 = Math.max(ax, bx, cx), y1 = Math.max(ay, by, cy); let p = c.next; while (p !== a) { if (p.x >= x0 && p.x <= x1 && p.y >= y0 && p.y <= y1 && pointInTriangleExceptFirst(ax, ay, bx, by, cx, cy, p.x, p.y) && area(p.prev, p, p.next) >= 0) return false; p = p.next; } return true; } function isEarHashed(ear, minX, minY, invSize) { const a = ear.prev, b = ear, c = ear.next; if (area(a, b, c) >= 0) return false; // reflex, can't be an ear const ax = a.x, bx = b.x, cx = c.x, ay = a.y, by = b.y, cy = c.y; // triangle bbox const x0 = Math.min(ax, bx, cx), y0 = Math.min(ay, by, cy), x1 = Math.max(ax, bx, cx), y1 = Math.max(ay, by, cy); // z-order range for the current triangle bbox; const minZ = zOrder(x0, y0, minX, minY, invSize), maxZ = zOrder(x1, y1, minX, minY, invSize); let p = ear.prevZ, n = ear.nextZ; // look for points inside the triangle in both directions while (p && p.z >= minZ && n && n.z <= maxZ) { if (p.x >= x0 && p.x <= x1 && p.y >= y0 && p.y <= y1 && p !== a && p !== c && pointInTriangleExceptFirst(ax, ay, bx, by, cx, cy, p.x, p.y) && area(p.prev, p, p.next) >= 0) return false; p = p.prevZ; if (n.x >= x0 && n.x <= x1 && n.y >= y0 && n.y <= y1 && n !== a && n !== c && pointInTriangleExceptFirst(ax, ay, bx, by, cx, cy, n.x, n.y) && area(n.prev, n, n.next) >= 0) return false; n = n.nextZ; } // look for remaining points in decreasing z-order while (p && p.z >= minZ) { if (p.x >= x0 && p.x <= x1 && p.y >= y0 && p.y <= y1 && p !== a && p !== c && pointInTriangleExceptFirst(ax, ay, bx, by, cx, cy, p.x, p.y) && area(p.prev, p, p.next) >= 0) return false; p = p.prevZ; } // look for remaining points in increasing z-order while (n && n.z <= maxZ) { if (n.x >= x0 && n.x <= x1 && n.y >= y0 && n.y <= y1 && n !== a && n !== c && pointInTriangleExceptFirst(ax, ay, bx, by, cx, cy, n.x, n.y) && area(n.prev, n, n.next) >= 0) return false; n = n.nextZ; } return true; } // go through all polygon nodes and cure small local self-intersections function cureLocalIntersections(start, triangles) { let p = start; do { const a = p.prev, b = p.next.next; if (!equals$1(a, b) && intersects(a, p, p.next, b) && locallyInside(a, b) && locallyInside(b, a)) { triangles.push(a.i, p.i, b.i); // remove two nodes involved removeNode(p); removeNode(p.next); p = start = b; } p = p.next; } while (p !== start); return filterPoints(p); } // try splitting polygon into two and triangulate them independently function splitEarcut(start, triangles, dim, minX, minY, invSize) { // look for a valid diagonal that divides the polygon into two let a = start; do { let b = a.next.next; while (b !== a.prev) { if (a.i !== b.i && isValidDiagonal(a, b)) { // split the polygon in two by the diagonal let c = splitPolygon(a, b); // filter colinear points around the cuts a = filterPoints(a, a.next); c = filterPoints(c, c.next); // run earcut on each half earcutLinked(a, triangles, dim, minX, minY, invSize, 0); earcutLinked(c, triangles, dim, minX, minY, invSize, 0); return; } b = b.next; } a = a.next; } while (a !== start); } // link every hole into the outer loop, producing a single-ring polygon without holes function eliminateHoles(data, holeIndices, outerNode, dim) { const queue = []; for (let i = 0, len = holeIndices.length; i < len; i++) { const start = holeIndices[i] * dim; const end = i < len - 1 ? holeIndices[i + 1] * dim : data.length; const list = linkedList(data, start, end, dim, false); if (list === list.next) list.steiner = true; queue.push(getLeftmost(list)); } queue.sort(compareXYSlope); // process holes from left to right for (let i = 0; i < queue.length; i++) { outerNode = eliminateHole(queue[i], outerNode); } return outerNode; } function compareXYSlope(a, b) { let result = a.x - b.x; // when the left-most point of 2 holes meet at a vertex, sort the holes counterclockwise so that when we find // the bridge to the outer shell is always the point that they meet at. if (result === 0) { result = a.y - b.y; if (result === 0) { const aSlope = (a.next.y - a.y) / (a.next.x - a.x); const bSlope = (b.next.y - b.y) / (b.next.x - b.x); result = aSlope - bSlope; } } return result; } // find a bridge between vertices that connects hole with an outer ring and and link it function eliminateHole(hole, outerNode) { const bridge = findHoleBridge(hole, outerNode); if (!bridge) { return outerNode; } const bridgeReverse = splitPolygon(bridge, hole); // filter collinear points around the cuts filterPoints(bridgeReverse, bridgeReverse.next); return filterPoints(bridge, bridge.next); } // David Eberly's algorithm for finding a bridge between hole and outer polygon function findHoleBridge(hole, outerNode) { let p = outerNode; const hx = hole.x; const hy = hole.y; let qx = -Infinity; let m; // find a segment intersected by a ray from the hole's leftmost point to the left; // segment's endpoint with lesser x will be potential connection point // unless they intersect at a vertex, then choose the vertex if (equals$1(hole, p)) return p; do { if (equals$1(hole, p.next)) return p.next; else if (hy <= p.y && hy >= p.next.y && p.next.y !== p.y) { const x = p.x + (hy - p.y) * (p.next.x - p.x) / (p.next.y - p.y); if (x <= hx && x > qx) { qx = x; m = p.x < p.next.x ? p : p.next; if (x === hx) return m; // hole touches outer segment; pick leftmost endpoint } } p = p.next; } while (p !== outerNode); if (!m) return null; // look for points inside the triangle of hole point, segment intersection and endpoint; // if there are no points found, we have a valid connection; // otherwise choose the point of the minimum angle with the ray as connection point const stop = m; const mx = m.x; const my = m.y; let tanMin = Infinity; p = m; do { if (hx >= p.x && p.x >= mx && hx !== p.x && pointInTriangle(hy < my ? hx : qx, hy, mx, my, hy < my ? qx : hx, hy, p.x, p.y)) { const tan = Math.abs(hy - p.y) / (hx - p.x); // tangential if (locallyInside(p, hole) && (tan < tanMin || (tan === tanMin && (p.x > m.x || (p.x === m.x && sectorContainsSector(m, p)))))) { m = p; tanMin = tan; } } p = p.next; } while (p !== stop); return m; } // whether sector in vertex m contains sector in vertex p in the same coordinates function sectorContainsSector(m, p) { return area(m.prev, m, p.prev) < 0 && area(p.next, m, m.next) < 0; } // interlink polygon nodes in z-order function indexCurve(start, minX, minY, invSize) { let p = start; do { if (p.z === 0) p.z = zOrder(p.x, p.y, minX, minY, invSize); p.prevZ = p.prev; p.nextZ = p.next; p = p.next; } while (p !== start); p.prevZ.nextZ = null; p.prevZ = null; sortLinked(p); } // Simon Tatham's linked list merge sort algorithm // http://www.chiark.greenend.org.uk/~sgtatham/algorithms/listsort.html function sortLinked(list) { let numMerges; let inSize = 1; do { let p = list; let e; list = null; let tail = null; numMerges = 0; while (p) { numMerges++; let q = p; let pSize = 0; for (let i = 0; i < inSize; i++) { pSize++; q = q.nextZ; if (!q) break; } let qSize = inSize; while (pSize > 0 || (qSize > 0 && q)) { if (pSize !== 0 && (qSize === 0 || !q || p.z <= q.z)) { e = p; p = p.nextZ; pSize--; } else { e = q; q = q.nextZ; qSize--; } if (tail) tail.nextZ = e; else list = e; e.prevZ = tail; tail = e; } p = q; } tail.nextZ = null; inSize *= 2; } while (numMerges > 1); return list; } // z-order of a point given coords and inverse of the longer side of data bbox function zOrder(x, y, minX, minY, invSize) { // coords are transformed into non-negative 15-bit integer range x = (x - minX) * invSize | 0; y = (y - minY) * invSize | 0; x = (x | (x << 8)) & 0x00FF00FF; x = (x | (x << 4)) & 0x0F0F0F0F; x = (x | (x << 2)) & 0x33333333; x = (x | (x << 1)) & 0x55555555; y = (y | (y << 8)) & 0x00FF00FF; y = (y | (y << 4)) & 0x0F0F0F0F; y = (y | (y << 2)) & 0x33333333; y = (y | (y << 1)) & 0x55555555; return x | (y << 1); } // find the leftmost node of a polygon ring function getLeftmost(start) { let p = start, leftmost = start; do { if (p.x < leftmost.x || (p.x === leftmost.x && p.y < leftmost.y)) leftmost = p; p = p.next; } while (p !== start); return leftmost; } // check if a point lies within a convex triangle function pointInTriangle(ax, ay, bx, by, cx, cy, px, py) { return (cx - px) * (ay - py) >= (ax - px) * (cy - py) && (ax - px) * (by - py) >= (bx - px) * (ay - py) && (bx - px) * (cy - py) >= (cx - px) * (by - py); } // check if a point lies within a convex triangle but false if its equal to the first point of the triangle function pointInTriangleExceptFirst(ax, ay, bx, by, cx, cy, px, py) { return !(ax === px && ay === py) && pointInTriangle(ax, ay, bx, by, cx, cy, px, py); } // check if a diagonal between two polygon nodes is valid (lies in polygon interior) function isValidDiagonal(a, b) { return a.next.i !== b.i && a.prev.i !== b.i && !intersectsPolygon(a, b) && // dones't intersect other edges (locallyInside(a, b) && locallyInside(b, a) && middleInside(a, b) && // locally visible (area(a.prev, a, b.prev) || area(a, b.prev, b)) || // does not create opposite-facing sectors equals$1(a, b) && area(a.prev, a, a.next) > 0 && area(b.prev, b, b.next) > 0); // special zero-length case } // signed area of a triangle function area(p, q, r) { return (q.y - p.y) * (r.x - q.x) - (q.x - p.x) * (r.y - q.y); } // check if two points are equal function equals$1(p1, p2) { return p1.x === p2.x && p1.y === p2.y; } // check if two segments intersect function intersects(p1, q1, p2, q2) { const o1 = sign(area(p1, q1, p2)); const o2 = sign(area(p1, q1, q2)); const o3 = sign(area(p2, q2, p1)); const o4 = sign(area(p2, q2, q1)); if (o1 !== o2 && o3 !== o4) return true; // general case if (o1 === 0 && onSegment(p1, p2, q1)) return true; // p1, q1 and p2 are collinear and p2 lies on p1q1 if (o2 === 0 && onSegment(p1, q2, q1)) return true; // p1, q1 and q2 are collinear and q2 lies on p1q1 if (o3 === 0 && onSegment(p2, p1, q2)) return true; // p2, q2 and p1 are collinear and p1 lies on p2q2 if (o4 === 0 && onSegment(p2, q1, q2)) return true; // p2, q2 and q1 are collinear and q1 lies on p2q2 return false; } // for collinear points p, q, r, check if point q lies on segment pr function onSegment(p, q, r) { return q.x <= Math.max(p.x, r.x) && q.x >= Math.min(p.x, r.x) && q.y <= Math.max(p.y, r.y) && q.y >= Math.min(p.y, r.y); } function sign(num) { return num > 0 ? 1 : num < 0 ? -1 : 0; } // check if a polygon diagonal intersects any polygon segments function intersectsPolygon(a, b) { let p = a; do { if (p.i !== a.i && p.next.i !== a.i && p.i !== b.i && p.next.i !== b.i && intersects(p, p.next, a, b)) return true; p = p.next; } while (p !== a); return false; } // check if a polygon diagonal is locally inside the polygon function locallyInside(a, b) { return area(a.prev, a, a.next) < 0 ? area(a, b, a.next) >= 0 && area(a, a.prev, b) >= 0 : area(a, b, a.prev) < 0 || area(a, a.next, b) < 0; } // check if the middle point of a polygon diagonal is inside the polygon function middleInside(a, b) { let p = a; let inside = false; const px = (a.x + b.x) / 2; const py = (a.y + b.y) / 2; do { if (((p.y > py) !== (p.next.y > py)) && p.next.y !== p.y && (px < (p.next.x - p.x) * (py - p.y) / (p.next.y - p.y) + p.x)) inside = !inside; p = p.next; } while (p !== a); return inside; } // link two polygon vertices with a bridge; if the vertices belong to the same ring, it splits polygon into two; // if one belongs to the outer ring and another to a hole, it merges it into a single ring function splitPolygon(a, b) { const a2 = createNode(a.i, a.x, a.y), b2 = createNode(b.i, b.x, b.y), an = a.next, bp = b.prev; a.next = b; b.prev = a; a2.next = an; an.prev = a2; b2.next = a2; a2.prev = b2; bp.next = b2; b2.prev = bp; return b2; } // create a node and optionally link it with previous one (in a circular doubly linked list) function insertNode(i, x, y, last) { const p = createNode(i, x, y); if (!last) { p.prev = p; p.next = p; } else { p.next = last.next; p.prev = last; last.next.prev = p; last.next = p; } return p; } function removeNode(p) { p.next.prev = p.prev; p.prev.next = p.next; if (p.prevZ) p.prevZ.nextZ = p.nextZ; if (p.nextZ) p.nextZ.prevZ = p.prevZ; } function createNode(i, x, y) { return { i, // vertex index in coordinates array x, y, // vertex coordinates prev: null, // previous and next vertex nodes in a polygon ring next: null, z: 0, // z-order curve value prevZ: null, // previous and next nodes in z-order nextZ: null, steiner: false // indicates whether this is a steiner point }; } function signedArea(data, start, end, dim) { let sum = 0; for (let i = start, j = end - dim; i < end; i += dim) { sum += (data[j] - data[i]) * (data[i + 1] + data[j + 1]); j = i; } return sum; } const epsilon = 1e-6; const worldUp = [0, 1, 0]; const worldRight = [1, 0, 0]; const tempVec3a$a = math.vec3(); const tempVec3b$7 = math.vec3(); const tempVec3c$6 = math.vec3(); const tempVec3d$1 = math.vec3(); const planeOff = math.vec3(); function pointsEqual(p1, p2) { return ( Math.abs(p1[0] - p2[0]) < epsilon && Math.abs(p1[1] - p2[1]) < epsilon && Math.abs(p1[2] - p2[2]) < epsilon ); } /** * @desc Implements hatching for Solid objects on a {@link Scene}. * * [[Run this example](https://xeokit.github.io/xeokit-sdk/examples/slicing/SectionPlanesPlugin_Duplex_SectionCaps.html)] * * ##Overview * * The WebGL implementation for capping sliced 3D objects works by first calculating intersection segments where a cutting * plane meets the object's edges. These segments form a contour that is triangulated using the Earcut algorithm, which * handles any internal holes efficiently. The resulting triangulated cap is then integrated into the original mesh with * appropriate normals and UVs. * * ##Usage * * In the example, we'll start by enabling readable geometry on the viewer. * * Then we'll position the camera, and configure the near and far perspective and orthographic * clipping planes. Finally, we'll use {@link XKTLoaderPlugin} to load the Duplex model. * * ````javascript * const viewer = new Viewer({ * canvasId: "myCanvas", * transparent: true, * readableGeometryEnabled: true * }); * * viewer.camera.eye = [-2.341298674548419, 22.43987089731119, 7.236688436028655]; * viewer.camera.look = [4.399999999999963, 3.7240000000000606, 8.899000000000006]; * viewer.camera.up = [0.9102954845584759, 0.34781746407929504, 0.22446635042673466]; * * const cameraControl = viewer.cameraControl; * cameraControl.navMode = "orbit"; * cameraControl.followPointer = true; * * const xktLoader = new XKTLoaderPlugin(viewer); * * var t0 = performance.now(); * * document.getElementById("time").innerHTML = "Loading model..."; * * const sceneModel = xktLoader.load({ * id: "myModel", * src: "../../assets/models/xkt/v10/glTF-Embedded/Duplex_A_20110505.glTFEmbedded.xkt", * edges: true * }); * * sceneModel.on("loaded", () => { * * var t1 = performance.now(); * document.getElementById("time").innerHTML = "Model loaded in " + Math.floor(t1 - t0) / 1000.0 + " seconds
Objects: " + sceneModel.numEntities; * * //------------------------------------------------------------------------------------------------------------------ * // Add caps materials to all objects inside the loaded model that have an opacity equal to or above 0.7 * //------------------------------------------------------------------------------------------------------------------ * const opacityThreshold = 0.7; * const material = new PhongMaterial(viewer.scene,{ * diffuse: [1.0, 0.0, 0.0], * backfaces: true * }); * addCapsMaterialsToAllObjects(sceneModel, opacityThreshold, material); * * //------------------------------------------------------------------------------------------------------------------ * // Create a moving SectionPlane, that moves through the table models * //------------------------------------------------------------------------------------------------------------------ * * const sectionPlanes = new SectionPlanesPlugin(viewer, { * overviewCanvasId: "mySectionPlanesOverviewCanvas", * overviewVisible: true, * }); * * const sectionPlane = sectionPlanes.createSectionPlane({ * id: "mySectionPlane", * pos: [0.5, 2.5, 5.0], * dir: math.normalizeVec3([1.0, 0.01, 1]) * }); * * sectionPlanes.showControl(sectionPlane.id); * * window.viewer = viewer; * * }); * * function addCapsMaterialsToAllObjects(sceneModel, opacityThreshold, material) { * const allObjects = sceneModel.objects; * for(const key in allObjects){ * const object = allObjects[key]; * if(object.opacity >= opacityThreshold) * object.capMaterial = material; * } * } * ```` */ class SectionCaps { /** * @constructor */ constructor(scene) { this.scene = scene; this._resourcesAllocated = false; } _onCapMaterialUpdated(entityId, modelId) { if(!this._resourcesAllocated) { this._resourcesAllocated = true; this._sectionPlanes = []; this._sceneModelsData = {}; this._dirtyMap = {}; this._prevIntersectionModelsMap = {}; this._sectionPlaneTimeout = null; this._updateTimeout = null; const handleSectionPlane = (sectionPlane) => { const onSectionPlaneUpdated = () => { this._setAllDirty(true); this._update(); }; this._sectionPlanes.push(sectionPlane); sectionPlane.on('pos', onSectionPlaneUpdated); sectionPlane.on('dir', onSectionPlaneUpdated); sectionPlane.on('active', onSectionPlaneUpdated); sectionPlane.once('destroyed', (() => { const sectionPlaneId = sectionPlane.id; if (sectionPlaneId) { this._sectionPlanes = this._sectionPlanes.filter((sectionPlane) => sectionPlane.id !== sectionPlaneId); this._update(); } }).bind(this)); }; for(const key in this.scene.sectionPlanes){ handleSectionPlane(this.scene.sectionPlanes[key]); } this._onSectionPlaneCreated = this.scene.on('sectionPlaneCreated', handleSectionPlane); this._onTick = this.scene.on("tick", () => { //on ticks we only check if there is a model that we have saved vertices for, //but it's no more available on the scene, or if its visibility changed let dirty = false; for(const sceneModelId in this._sceneModelsData) { if(!this.scene.models[sceneModelId]){ delete this._sceneModelsData[sceneModelId]; dirty = true; } else if (this._sceneModelsData[sceneModelId].visible !== (!!this.scene.models[sceneModelId].visible)) { this._sceneModelsData[sceneModelId].visible = !!this.scene.models[sceneModelId].visible; dirty = true; } } if (dirty) { this._update(); } }); } if(!this._dirtyMap[modelId]) this._dirtyMap[modelId] = new Map(); this._dirtyMap[modelId].set(entityId, true); this._update(); } _update() { clearTimeout(this._updateTimeout); this._deletePreviousModels(); this._updateTimeout = setTimeout(() => { clearTimeout(this._updateTimeout); const sceneModels = Object.values(this.scene.models).filter(sceneModel => sceneModel.visible); this._addHatches(sceneModels, this._sectionPlanes.filter(sectionPlane => sectionPlane.active)); this._setAllDirty(false); }, 100); } _setAllDirty(value) { for(const key in this._dirty) { this._dirtyMap[key].forEach((_, key2) => this._dirtyMap[key].set(key2, value)); } } _addHatches(sceneModels, planes) { planes.forEach((plane) => { sceneModels.forEach((sceneModel) => { if(!this._doesPlaneIntersectBoundingBox(sceneModel.aabb, plane)) return; if(!this._dirtyMap[sceneModel.id]) return; //#region calculating segments in unsorted way //we calculate the segments by intersecting plane with each triangle const unsortedSegments = new Map(); const objects = sceneModel.objects; // Preallocate arrays for triangle vertices to avoid repeated allocation const triangle = [ math.vec3(), math.vec3(), math.vec3() ]; this._dirtyMap[sceneModel.id].forEach((isDirty, objectId) => { if (!isDirty) { return; } const object = objects[objectId]; if(!this._doesPlaneIntersectBoundingBox(object.aabb, plane)) return; if(!this._sceneModelsData[sceneModel.id]) { const aabb = sceneModel.aabb; this._sceneModelsData[sceneModel.id] = { verticesMap: new Map(), indicesMap: new Map(), // modelOrigin is critical to use when handling models with large coordinates. // See XCD-306 and examples/slicing/SectionCaps_at_distance.html for more details. modelOrigin: math.vec3([ (aabb[0] + aabb[3]) / 2, (aabb[1] + aabb[4]) / 2, (aabb[2] + aabb[5]) / 2 ]) }; } const sceneModelData = this._sceneModelsData[sceneModel.id]; const modelOrigin = sceneModelData.modelOrigin; if(!sceneModelData.verticesMap.has(objectId)) { const isSolid = object.meshes[0].isSolid(); const vertices = [ ]; const indices = [ ]; if(isSolid && object.capMaterial) { object.getEachVertex(v => vertices.push(v[0]-modelOrigin[0], v[1]-modelOrigin[1], v[2]-modelOrigin[2])); object.getEachIndex(i => indices.push(i)); } sceneModelData.verticesMap.set(objectId, vertices); sceneModelData.indicesMap.set(objectId, indices); } const vertices = sceneModelData.verticesMap.get(objectId); const indices = sceneModelData.indicesMap.get(objectId); const planeDist = -math.dotVec3(math.subVec3(plane.pos, modelOrigin, tempVec3a$a), plane.dir); const capSegments = []; const vertCount = indices.length; for (let i = 0; i < vertCount; i += 3) { // Reuse triangle buffer instead of creating new arrays for (let j = 0; j < 3; j++) { const idx = indices[i + j] * 3; triangle[j][0] = vertices[idx]; triangle[j][1] = vertices[idx + 1]; triangle[j][2] = vertices[idx + 2]; } // Early null check if (!triangle[0][0] && !triangle[0][1] && !triangle[0][2]) continue; const intersections = []; for (let i = 0; i < 3; i++) { const p1 = triangle[i]; const p2 = triangle[(i + 1) % 3]; const d1 = planeDist + math.dotVec3(plane.dir, p1); const d2 = planeDist + math.dotVec3(plane.dir, p2); if (d1 * d2 > 0) continue; const t = -d1 / (d2 - d1); intersections.push(math.lerpVec3(t, 0, 1, p1, p2, math.vec3())); } if(intersections.length === 2) capSegments.push(intersections); } if (capSegments.length > 0) { unsortedSegments.set(objectId, capSegments); } }); //#endregion //#region sorting the segments const orderedSegments = new Map(); unsortedSegments.forEach((unsortedSegment, segmentedId) => { orderedSegments.set(segmentedId, [ [ unsortedSegment[0] //this is also an array of two vectors ] ]); unsortedSegment.splice(0, 1); let index = 0; while (unsortedSegment.length > 0) { const lastPoint = orderedSegments.get(segmentedId)[index][orderedSegments.get(segmentedId)[index].length - 1][1]; let found = false; for (let i = 0; i < unsortedSegment.length; i++) { const [start, end] = unsortedSegment[i]; if (pointsEqual(lastPoint, start)) { orderedSegments.get(segmentedId)[index].push(unsortedSegment[i]); unsortedSegment.splice(i, 1); found = true; break; } else if (pointsEqual(lastPoint, end)) { orderedSegments.get(segmentedId)[index].push([end, start]); unsortedSegment.splice(i, 1); found = true; break; } } if (!found) { if (pointsEqual(lastPoint, orderedSegments.get(segmentedId)[index][0][0])) { if (unsortedSegment.length > 1) { orderedSegments.get(segmentedId).push([ unsortedSegments.get(segmentedId)[0] ]); unsortedSegment.splice(0, 1); index++; continue; } } } if (!found) { // console.error(`Could not find a matching segment. Loop may not be closed. Key: ${key}`); break; } } }); //#endregion //#region projecting the segments to 2D const projectedSegments = new Map(); orderedSegments.forEach((orderedSegment, key) => { const arr = []; for (let i = 0; i < orderedSegment.length; i++) { arr.push([]); orderedSegment[i].forEach((segment) => { arr[i].push([ this._projectTo2D(segment[0], plane.dir), this._projectTo2D(segment[1], plane.dir) ]); }); } projectedSegments.set(key, arr); }); //#endregion //#region creating caps using earcut and then projecting them back to 3D const caps = new Map(); let arr; projectedSegments.forEach((segment, segmentId) => { const modelOrigin = this._sceneModelsData[sceneModel.id].modelOrigin; arr = []; const loops = segment; // Group related loops (outer boundaries with their holes) const groupedLoops = []; const used = new Set(); for (let i = 0; i < loops.length; i++) { if (used.has(i)) continue; const group = [loops[i]]; used.add(i); // Check remaining loops for (let j = i + 1; j < loops.length; j++) { if (used.has(j)) continue; if (this._isLoopInside(loops[i], loops[j]) || this._isLoopInside(loops[j], loops[i])) { group.push(loops[j]); used.add(j); } } groupedLoops.push(group); } // Process each group separately groupedLoops.forEach(group => { // Convert the segments into a flat array of vertices and find holes const vertices = []; const holes = []; let currentIndex = 0; // First, determine which loop has the largest area - this will be our outer boundary const areas = group.map(loop => { let area = 0; for (let i = 0; i < loop.length; i++) { const j = (i + 1) % loop.length; area += loop[i][0][0] * loop[j][0][1]; area -= loop[j][0][0] * loop[i][0][1]; } return Math.abs(area) / 2; }); // Find index of the loop with maximum area const outerLoopIndex = areas.indexOf(Math.max(...areas)); // Add the outer boundary first group[outerLoopIndex].forEach(segment => { vertices.push(segment[0][0], segment[0][1]); currentIndex += 2; }); // Then add all other loops as holes for (let i = 0; i < group.length; i++) { if (i !== outerLoopIndex) { // Store the starting vertex index for this hole holes.push(currentIndex / 2); group[i].forEach(segment => { vertices.push(segment[0][0], segment[0][1]); currentIndex += 2; }); } } // Triangulate using earcut const triangles = earcut(vertices, holes); // // Convert triangulated 2D points back to 3D const cap3D = []; // Process each triangle for (let i = 0; i < triangles.length; i += 3) { const triangle = []; // Convert each vertex for (let j = 0; j < 3; j++) { const idx = triangles[i + j] * 2; const point2D = [vertices[idx], vertices[idx + 1]]; const point3D = this._convertTo3D(point2D, plane, modelOrigin); triangle.push(point3D); } cap3D.push(triangle); } arr.push(cap3D); }); caps.set(segmentId, arr); }); //#endregion //#region converting caps to geometry const geometryData = new Map(); caps.forEach((cap, capId) => { arr = []; cap.forEach(capTriangles => { // Create a vertex map to reuse vertices const vertexMap = new Map(); const vertices = []; const indices = []; let currentIndex = 0; capTriangles.forEach(triangle => { const triangleIndices = []; // Process each vertex of the triangle triangle.forEach(vertex => { // Create a key for the vertex to check for duplicates const vertexKey = `${vertex[0].toFixed(6)},${vertex[1].toFixed(6)},${vertex[2].toFixed(6)}`; if (vertexMap.has(vertexKey)) { // Reuse existing vertex triangleIndices.push(vertexMap.get(vertexKey)); } else { // Add new vertex vertices.push(vertex[0], vertex[1], vertex[2]); vertexMap.set(vertexKey, currentIndex); triangleIndices.push(currentIndex); currentIndex++; } }); // Add triangle indices indices.push(...triangleIndices); }); arr.push({ positions: vertices, indices: indices }); }); geometryData.set(capId, arr); }); //#endregion //#region adding meshes to the scene if(!this._prevIntersectionModelsMap[sceneModel.id]) this._prevIntersectionModelsMap[sceneModel.id] = new Map(); // Cache plane direction values math.mulVec3Scalar(plane.dir, 0.001, planeOff); // Use dedicated planeOff, as tempVec* are overwritten by _createUVs geometryData.forEach((geometries, objectId) => { const modelOrigin = this._sceneModelsData[sceneModel.id].modelOrigin; const meshArray = new Array(geometries.size); // Pre-allocate array with known size let meshIndex = 0; geometries.forEach((geometry, index) => { const vertices = geometry.positions; const indices = geometry.indices; vertices.length; // Build normals and UVs in parallel if possible const meshNormals = math.buildNormals(vertices, indices); const uvs = this._createUVs(vertices, plane, modelOrigin); // Create mesh with transformed vertices meshArray[meshIndex++] = new Mesh(this.scene, { id: `${plane.id}-${objectId}-${index}`, geometry: new ReadableGeometry(this.scene, { primitive: 'triangles', positions: vertices, // Only copy what we need indices, normals: meshNormals, uv: uvs }), origin: math.addVec3(modelOrigin, planeOff, tempVec3a$a), position: [0, 0, 0], rotation: [0, 0, 0], material: sceneModel.objects[objectId].capMaterial }); }); if(this._prevIntersectionModelsMap[sceneModel.id].has(objectId)) { this._prevIntersectionModelsMap[sceneModel.id].get(objectId).push(...meshArray); } else this._prevIntersectionModelsMap[sceneModel.id].set(objectId, meshArray); }); //#endregion }); }); } _doesPlaneIntersectBoundingBox(bb, plane) { const min = [bb[0], bb[1], bb[2]]; const max = [bb[3], bb[4], bb[5]]; const corners = [ [min[0], min[1], min[2]], // 000 [max[0], min[1], min[2]], // 100 [min[0], max[1], min[2]], // 010 [max[0], max[1], min[2]], // 110 [min[0], min[1], max[2]], // 001 [max[0], min[1], max[2]], // 101 [min[0], max[1], max[2]], // 011 [max[0], max[1], max[2]] // 111 ]; // Calculate distance from each corner to the plane let hasPositive = false; let hasNegative = false; for (const corner of corners) { const distance = plane.dist + math.dotVec3(plane.dir, corner); if (distance > 0) hasPositive = true; if (distance < 0) hasNegative = true; // If we found points on both sides, the plane intersects the box if (hasPositive && hasNegative) return true; } // If all points are on the same side, no intersection return false; } //not used but kept for debugging _buildLines(sortedSegments) { for (const key in sortedSegments) { for (let i = 0; i < sortedSegments[key].length; i++) { const segments = sortedSegments[key][i]; if (segments.length <= 0) continue; segments.forEach((segment, index) => { new Mesh(this.scene, { clippable: false, geometry: new ReadableGeometry(this.scene, buildLineGeometry({ startPoint: segment[0], endPoint: segment[1], })), material: new PhongMaterial(this.scene, { emissive: [1, 0, 0] }) }); }); } } } _projectTo2D(point, normal) { let u; if (Math.abs(normal[0]) > Math.abs(normal[1])) u = [-normal[2], 0, normal[0]]; else u = [0, normal[2], -normal[1]]; u = math.normalizeVec3(u); const normalTemp = math.vec3(normal); const cross = math.cross3Vec3(normalTemp, u); const v = math.normalizeVec3(cross); const x = math.dotVec3(point, u); const y = math.dotVec3(point, v); return [x, y] } _isLoopInside(loop1, loop2) { // Simple point-in-polygon test using the first point of loop1 const point = loop1[0][0]; // First point of first segment let inside = false; for (let i = 0, j = loop2.length - 1; i < loop2.length; j = i++) { const xi = loop2[i][0][0], yi = loop2[i][0][1]; const xj = loop2[j][0][0], yj = loop2[j][0][1]; const intersect = ((yi > point[1]) !== (yj > point[1])) && (point[0] < (xj - xi) * (point[1] - yi) / (yj - yi) + xi); if (intersect) inside = !inside; } return inside; } _convertTo3D(point2D, plane, origin) { // Reconstruct the same basis vectors used in _projectTo2D let u, normal = plane.dir, planePosition = plane.pos; if (Math.abs(normal[0]) > Math.abs(normal[1])) { u = [-normal[2], 0, normal[0]]; } else { u = [0, normal[2], -normal[1]]; } u = math.normalizeVec3(u); const normalTemp = math.vec3(normal); const cross = math.cross3Vec3(normalTemp, u); const v = math.normalizeVec3(cross); // Reconstruct 3D point using the basis vectors const x = point2D[0]; const y = point2D[1]; const result = [ u[0] * x + v[0] * y, u[1] * x + v[1] * y, u[2] * x + v[2] * y ]; // Project the point onto the cutting plane const t = math.dotVec3(normal, [ planePosition[0] - result[0] - origin[0], planePosition[1] - result[1] - origin[1], planePosition[2] - result[2] - origin[2] ]); return [ result[0] + normal[0] * t, result[1] + normal[1] * t, result[2] + normal[2] * t ]; } _deletePreviousModels() { for(const sceneModelId in this._prevIntersectionModelsMap) { const objects = this._prevIntersectionModelsMap[sceneModelId]; objects.forEach((value, objectId) => { if(this._dirtyMap[sceneModelId].get(objectId)) { value.forEach((mesh) => { mesh.destroy(); }); this._prevIntersectionModelsMap[sceneModelId].delete(objectId); } }); if(this._prevIntersectionModelsMap[sceneModelId].size <= 0) delete this._prevIntersectionModelsMap[sceneModelId]; } } _createUVs(vertices, plane, origin) { const O = plane.pos; const D = tempVec3a$a; D.set(plane.dir); math.normalizeVec3(D); const P = tempVec3b$7; const uvs = [ ]; for (let i = 0; i < vertices.length; i += 3) { P[0] = vertices[i] + origin[0]; P[1] = vertices[i + 1] + origin[1]; P[2] = vertices[i + 2] + origin[2]; // Project P onto the plane const OP = math.subVec3(P, O, tempVec3c$6); const dist = math.dotVec3(OP, D); math.subVec3(P, math.mulVec3Scalar(D, dist, tempVec3c$6), P); const right = ((Math.abs(math.dotVec3(D, worldUp)) < 0.999) ? math.cross3Vec3(D, worldUp, tempVec3c$6) : worldRight); const v = math.cross3Vec3(D, right, tempVec3c$6); math.normalizeVec3(v, v); const OP_proj = math.subVec3(P, O, P); uvs.push( math.dotVec3(OP_proj, math.normalizeVec3(math.cross3Vec3(v, D, tempVec3d$1))), math.dotVec3(OP_proj, v)); } return uvs; } destroy() { this._deletePreviousModels(); if(this._resourcesAllocated) { this.scene.off(this._onModelLoaded); this.scene.off(this._onModelUnloaded); this.scene.off(this._onSectionPlaneCreated); this.scene.off(this._onTick); } } } // Cached vars to avoid garbage collection function getEntityIDMap(scene, entityIds) { const map = {}; let entityId; let entity; for (let i = 0, len = entityIds.length; i < len; i++) { entityId = entityIds[i]; entity = scene.components[entityId]; if (!entity) { scene.warn("pick(): Component not found: " + entityId); continue; } if (!entity.isEntity) { scene.warn("pick(): Component is not an Entity: " + entityId); continue; } map[entityId] = true; } return map; } /** * Fired whenever a debug message is logged on a component within this Scene. * @event log * @param {String} value The debug message */ /** * Fired whenever an error is logged on a component within this Scene. * @event error * @param {String} value The error message */ /** * Fired whenever a warning is logged on a component within this Scene. * @event warn * @param {String} value The warning message */ /** * @desc Contains the components that comprise a 3D scene. * * * A {@link Viewer} has a single Scene, which it provides in {@link Viewer#scene}. * * Plugins like {@link AxisGizmoPlugin} also have their own private Scenes. * * Each Scene has a corresponding {@link MetaScene}, which the Viewer provides in {@link Viewer#metaScene}. * * ## Getting a Viewer's Scene * * ````javascript * var scene = viewer.scene; * ```` * * ## Creating and accessing Scene components * * As a brief introduction to creating Scene components, we'll create a {@link Mesh} that has a * {@link buildTorusGeometry} and a {@link PhongMaterial}: * * ````javascript * var teapotMesh = new Mesh(scene, { * id: "myMesh", // <<---------- ID automatically generated if not provided * geometry: new TorusGeometry(scene), * material: new PhongMaterial(scene, { * id: "myMaterial", * diffuse: [0.2, 0.2, 1.0] * }) * }); * * teapotMesh.scene.camera.eye = [45, 45, 45]; * ```` * * Find components by ID in their Scene's {@link Scene#components} map: * * ````javascript * var teapotMesh = scene.components["myMesh"]; * teapotMesh.visible = false; * * var teapotMaterial = scene.components["myMaterial"]; * teapotMaterial.diffuse = [1,0,0]; // Change to red * ```` * * A Scene also has a map of component instances for each {@link Component} subtype: * * ````javascript * var meshes = scene.types["Mesh"]; * var teapotMesh = meshes["myMesh"]; * teapotMesh.xrayed = true; * * var phongMaterials = scene.types["PhongMaterial"]; * var teapotMaterial = phongMaterials["myMaterial"]; * teapotMaterial.diffuse = [0,1,0]; // Change to green * ```` * * See {@link Node}, {@link Node} and {@link Model} for how to create and access more sophisticated content. * * ## Controlling the camera * * Use the Scene's {@link Camera} to control the current viewpoint and projection: * * ````javascript * var camera = myScene.camera; * * camera.eye = [-10,0,0]; * camera.look = [-10,0,0]; * camera.up = [0,1,0]; * * camera.projection = "perspective"; * camera.perspective.fov = 45; * //... * ```` * * ## Managing the canvas * * The Scene's {@link Canvas} component provides various conveniences relevant to the WebGL canvas, such * as firing resize events etc: * * ````javascript * var canvas = scene.canvas; * * canvas.on("boundary", function(boundary) { * //... * }); * ```` * * ## Picking * * Use {@link Scene#pick} to pick and raycast entites. * * For example, to pick a point on the surface of the closest entity at the given canvas coordinates: * * ````javascript * var pickResult = scene.pick({ * pickSurface: true, * canvasPos: [23, 131] * }); * * if (pickResult) { // Picked an entity * * var entity = pickResult.entity; * * var primitive = pickResult.primitive; // Type of primitive that was picked, usually "triangles" * var primIndex = pickResult.primIndex; // Position of triangle's first index in the picked Mesh's Geometry's indices array * var indices = pickResult.indices; // UInt32Array containing the triangle's vertex indices * var localPos = pickResult.localPos; // Float64Array containing the picked Local-space position on the triangle * var worldPos = pickResult.worldPos; // Float64Array containing the picked World-space position on the triangle * var viewPos = pickResult.viewPos; // Float64Array containing the picked View-space position on the triangle * var bary = pickResult.bary; // Float64Array containing the picked barycentric position within the triangle * var normal = pickResult.normal; // Float64Array containing the interpolated normal vector at the picked position on the triangle * var uv = pickResult.uv; // Float64Array containing the interpolated UV coordinates at the picked position on the triangle * } * ```` * * ## Pick masking * * We can use {@link Scene#pick}'s ````includeEntities```` and ````excludeEntities```` options to mask which {@link Mesh}es we attempt to pick. * * This is useful for picking through things, to pick only the Entities of interest. * * To pick only Entities ````"gearbox#77.0"```` and ````"gearbox#79.0"````, picking through any other Entities that are * in the way, as if they weren't there: * * ````javascript * var pickResult = scene.pick({ * canvasPos: [23, 131], * includeEntities: ["gearbox#77.0", "gearbox#79.0"] * }); * * if (pickResult) { * // Entity will always be either "gearbox#77.0" or "gearbox#79.0" * var entity = pickResult.entity; * } * ```` * * To pick any pickable Entity, except for ````"gearbox#77.0"```` and ````"gearbox#79.0"````, picking through those * Entities if they happen to be in the way: * * ````javascript * var pickResult = scene.pick({ * canvasPos: [23, 131], * excludeEntities: ["gearbox#77.0", "gearbox#79.0"] * }); * * if (pickResult) { * // Entity will never be "gearbox#77.0" or "gearbox#79.0" * var entity = pickResult.entity; * } * ```` * * See {@link Scene#pick} for more info on picking. * * ## Querying and tracking boundaries * * Getting a Scene's World-space axis-aligned boundary (AABB): * * ````javascript * var aabb = scene.aabb; // [xmin, ymin, zmin, xmax, ymax, zmax] * ```` * * Subscribing to updates to the AABB, which occur whenever {@link Entity}s are transformed, their * {@link ReadableGeometry}s have been updated, or the {@link Camera} has moved: * * ````javascript * scene.on("boundary", function() { * var aabb = scene.aabb; * }); * ```` * * Getting the AABB of the {@link Entity}s with the given IDs: * * ````JavaScript * scene.getAABB(); // Gets collective boundary of all Entities in the scene * scene.getAABB("saw"); // Gets boundary of an Object * scene.getAABB(["saw", "gearbox"]); // Gets collective boundary of two Objects * ```` * * See {@link Scene#getAABB} and {@link Entity} for more info on querying and tracking boundaries. * * ## Managing the viewport * * The Scene's {@link Viewport} component manages the WebGL viewport: * * ````javascript * var viewport = scene.viewport * viewport.boundary = [0, 0, 500, 400];; * ```` * * ## Controlling rendering * * You can configure a Scene to perform multiple "passes" (renders) per frame. This is useful when we want to render the * scene to multiple viewports, such as for stereo effects. * * In the example, below, we'll configure the Scene to render twice on each frame, each time to different viewport. We'll do this * with a callback that intercepts the Scene before each render and sets its {@link Viewport} to a * different portion of the canvas. By default, the Scene will clear the canvas only before the first render, allowing the * two views to be shown on the canvas at the same time. * * ````Javascript * var viewport = scene.viewport; * * // Configure Scene to render twice for each frame * scene.passes = 2; // Default is 1 * scene.clearEachPass = false; // Default is false * * // Render to a separate viewport on each render * * var viewport = scene.viewport; * viewport.autoBoundary = false; * * scene.on("rendering", function (e) { * switch (e.pass) { * case 0: * viewport.boundary = [0, 0, 200, 200]; // xmin, ymin, width, height * break; * * case 1: * viewport.boundary = [200, 0, 200, 200]; * break; * } * }); * * // We can also intercept the Scene after each render, * // (though we're not using this for anything here) * scene.on("rendered", function (e) { * switch (e.pass) { * case 0: * break; * * case 1: * break; * } * }); * ```` * * ## Gamma correction * * Within its shaders, xeokit performs shading calculations in linear space. * * By default, the Scene expects color textures (eg. {@link PhongMaterial#diffuseMap}, * {@link MetallicMaterial#baseColorMap} and {@link SpecularMaterial#diffuseMap}) to * be in pre-multipled gamma space, so will convert those to linear space before they are used in shaders. Other textures are * always expected to be in linear space. * * By default, the Scene will also gamma-correct its rendered output. * * You can configure the Scene to expect all those color textures to be linear space, so that it does not gamma-correct them: * * ````javascript * scene.gammaInput = false; * ```` * * You would still need to gamma-correct the output, though, if it's going straight to the canvas, so normally we would * leave that enabled: * * ````javascript * scene.gammaOutput = true; * ```` * * See {@link Texture} for more information on texture encoding and gamma. * * @class Scene */ class Scene extends Component { /** @private */ get type() { return "Scene"; } /** * @private * @constructor * @param {Viewer} viewer The Viewer this Scene belongs to. * @param {Object} cfg Scene configuration. * @param {String} [cfg.canvasId] ID of an existing HTML canvas for the {@link Scene#canvas} - either this or canvasElement is mandatory. When both values are given, the element reference is always preferred to the ID. * @param {HTMLCanvasElement} [cfg.canvasElement] Reference of an existing HTML canvas for the {@link Scene#canvas} - either this or canvasId is mandatory. When both values are given, the element reference is always preferred to the ID. * @param {HTMLElement} [cfg.keyboardEventsElement] Optional reference to HTML element on which key events should be handled. Defaults to the HTML Document. * @param {number} [cfg.numCachedSectionPlanes=0] Enhances the efficiency of SectionPlane creation by proactively allocating Viewer resources for a specified quantity * of SectionPlanes. Introducing this parameter streamlines the initial creation speed of SectionPlanes, particularly up to the designated quantity. This parameter internally * configures renderer logic for the specified number of SectionPlanes, eliminating the need for setting up logic with each SectionPlane creation and thereby enhancing * responsiveness. It is important to consider that each SectionPlane imposes rendering performance, so it is recommended to set this value to a quantity that aligns with * your expected usage. * @throws {String} Throws an exception when canvasId or canvasElement are missing or they aren't pointing to a valid HTMLCanvasElement. */ constructor(viewer, cfg = {}) { super(null, cfg); const canvas = cfg.canvasElement || document.querySelector(`#${cfg.canvasId}`); if (!(canvas instanceof HTMLCanvasElement)) { throw "Mandatory config expected: valid canvasId or canvasElement"; } /** * @type {{[key: string]: {wrapperFunc: Function, tickSubId: string}}} */ this._tickifiedFunctions = {}; const transparent = (!!cfg.transparent); const alphaDepthMask = (!!cfg.alphaDepthMask); this._aabbDirty = true; this._readableGeometry = !!cfg.readableGeometryEnabled; /** * The {@link Viewer} this Scene belongs to. * @type {Viewer} */ this.viewer = viewer; /** Decremented each frame, triggers occlusion test for occludable {@link Marker}s when zero. * @private * @type {number} */ this.occlusionTestCountdown = 0; /** The number of models currently loading. @property loading @final @type {Number} */ this.loading = 0; /** The epoch time (in milliseconds since 1970) when this Scene was instantiated. @property timeCreated @final @type {Number} */ this.startTime = (new Date()).getTime(); /** * Map of {@link Entity}s that represent models. * * Each {@link Entity} is mapped here by {@link Entity#id} when {@link Entity#isModel} is ````true````. * * @property models * @final * @type {{String:Entity}} */ this.models = {}; /** * Map of {@link Entity}s that represents objects. * * Each {@link Entity} is mapped here by {@link Entity#id} when {@link Entity#isObject} is ````true````. * * @property objects * @final * @type {{String:Entity}} */ this.objects = {}; this._numObjects = 0; /** * Map of currently visible {@link Entity}s that represent objects. * * An Entity represents an object if {@link Entity#isObject} is ````true````, and is visible when {@link Entity#visible} is true. * * @property visibleObjects * @final * @type {{String:Object}} */ this.visibleObjects = {}; this._numVisibleObjects = 0; /** * Map of currently xrayed {@link Entity}s that represent objects. * * An Entity represents an object if {@link Entity#isObject} is ````true````, and is xrayed when {@link Entity#xrayed} is true. * * Each {@link Entity} is mapped here by {@link Entity#id}. * * @property xrayedObjects * @final * @type {{String:Object}} */ this.xrayedObjects = {}; this._numXRayedObjects = 0; /** * Map of currently highlighted {@link Entity}s that represent objects. * * An Entity represents an object if {@link Entity#isObject} is ````true```` is true, and is highlighted when {@link Entity#highlighted} is true. * * Each {@link Entity} is mapped here by {@link Entity#id}. * * @property highlightedObjects * @final * @type {{String:Object}} */ this.highlightedObjects = {}; this._numHighlightedObjects = 0; /** * Map of currently selected {@link Entity}s that represent objects. * * An Entity represents an object if {@link Entity#isObject} is true, and is selected while {@link Entity#selected} is true. * * Each {@link Entity} is mapped here by {@link Entity#id}. * * @property selectedObjects * @final * @type {{String:Object}} */ this.selectedObjects = {}; this._numSelectedObjects = 0; /** * Map of currently colorized {@link Entity}s that represent objects. * * An Entity represents an object if {@link Entity#isObject} is ````true````. * * Each {@link Entity} is mapped here by {@link Entity#id}. * * @property colorizedObjects * @final * @type {{String:Object}} */ this.colorizedObjects = {}; this._numColorizedObjects = 0; /** * Map of {@link Entity}s that represent objects whose opacity was updated. * * An Entity represents an object if {@link Entity#isObject} is ````true````. * * Each {@link Entity} is mapped here by {@link Entity#id}. * * @property opacityObjects * @final * @type {{String:Object}} */ this.opacityObjects = {}; this._numOpacityObjects = 0; /** * Map of {@link Entity}s that represent objects whose {@link Entity#offset}s were updated. * * An Entity represents an object if {@link Entity#isObject} is ````true````. * * Each {@link Entity} is mapped here by {@link Entity#id}. * * @property offsetObjects * @final * @type {{String:Object}} */ this.offsetObjects = {}; this._numOffsetObjects = 0; // Cached ID arrays, lazy-rebuilt as needed when stale after map updates /** Lazy-regenerated ID lists. */ this._modelIds = null; this._objectIds = null; this._visibleObjectIds = null; this._xrayedObjectIds = null; this._highlightedObjectIds = null; this._selectedObjectIds = null; this._colorizedObjectIds = null; this._opacityObjectIds = null; this._offsetObjectIds = null; this._collidables = {}; // Components that contribute to the Scene AABB this._compilables = {}; // Components that require shader compilation this._needRecompile = false; /** * For each {@link Component} type, a map of IDs to {@link Component} instances of that type. * * @type {{String:{String:Component}}} */ this.types = {}; /** * The {@link Component}s within this Scene, each mapped to its {@link Component#id}. * * *@type {{String:Component}} */ this.components = {}; /** * The {@link SectionPlane}s in this Scene, each mapped to its {@link SectionPlane#id}. * * @type {{String:SectionPlane}} */ this.sectionPlanes = {}; /** * The {@link Light}s in this Scene, each mapped to its {@link Light#id}. * * @type {{String:Light}} */ this.lights = {}; /** * The {@link LightMap}s in this Scene, each mapped to its {@link LightMap#id}. * * @type {{String:LightMap}} */ this.lightMaps = {}; /** * The {@link ReflectionMap}s in this Scene, each mapped to its {@link ReflectionMap#id}. * * @type {{String:ReflectionMap}} */ this.reflectionMaps = {}; /** * The {@link Bitmap}s in this Scene, each mapped to its {@link Bitmap#id}. * * @type {{String:Bitmap}} */ this.bitmaps = {}; /** * The {@link LineSet}s in this Scene, each mapped to its {@link LineSet#id}. * * @type {{String:LineSet}} */ this.lineSets = {}; /** * The real world offset for this Scene * * @type {Number[]} */ this.realWorldOffset = cfg.realWorldOffset || new Float64Array([0, 0, 0]); /** * Manages the HTML5 canvas for this Scene. * * @type {Canvas} */ this.canvas = new Canvas(this, { dontClear: true, // Never destroy this component with Scene#clear(); canvas: canvas, spinnerElementId: cfg.spinnerElementId, transparent: transparent, webgl2: cfg.webgl2 !== false, contextAttr: cfg.contextAttr || {}, backgroundColor: cfg.backgroundColor, backgroundColorFromAmbientLight: cfg.backgroundColorFromAmbientLight, premultipliedAlpha: cfg.premultipliedAlpha }); this.canvas.on("boundary", () => { this.glRedraw(); }); this.canvas.on("webglContextFailed", () => { alert("xeokit failed to find WebGL!"); }); this._renderer = new Renderer$1(this, { transparent: transparent, alphaDepthMask: alphaDepthMask }); this._sectionPlanesState = new (function () { this.sectionPlanes = []; this.clippingCaps = false; this._numCachedSectionPlanes = 0; let hash = null; this.getHash = function () { if (hash) { return hash; } const numAllocatedSectionPlanes = this.getNumAllocatedSectionPlanes(); this.sectionPlanes; if (numAllocatedSectionPlanes === 0) { return this.hash = ";"; } const hashParts = []; for (let i = 0, len = numAllocatedSectionPlanes; i < len; i++) { hashParts.push("cp"); } hashParts.push(";"); hash = hashParts.join(""); return hash; }; this.addSectionPlane = function (sectionPlane) { this.sectionPlanes.push(sectionPlane); hash = null; }; this.removeSectionPlane = function (sectionPlane) { for (let i = 0, len = this.sectionPlanes.length; i < len; i++) { if (this.sectionPlanes[i].id === sectionPlane.id) { this.sectionPlanes.splice(i, 1); hash = null; return; } } }; this.setNumCachedSectionPlanes = function (numCachedSectionPlanes) { this._numCachedSectionPlanes = numCachedSectionPlanes; hash = null; }; this.getNumCachedSectionPlanes = function () { return this._numCachedSectionPlanes; }; this.getNumAllocatedSectionPlanes = function () { const num = this.sectionPlanes.length; return (num > this._numCachedSectionPlanes) ? num : this._numCachedSectionPlanes; }; })(); this._sectionPlanesState.setNumCachedSectionPlanes(cfg.numCachedSectionPlanes || 0); this._lightsState = new (function () { const DEFAULT_AMBIENT = math.vec4([0, 0, 0, 0]); const ambientColorIntensity = math.vec4(); this.lights = []; this.reflectionMaps = []; this.lightMaps = []; let hash = null; let ambientLight = null; this.getHash = function () { if (hash) { return hash; } const hashParts = []; const lights = this.lights; let light; for (let i = 0, len = lights.length; i < len; i++) { light = lights[i]; hashParts.push("/"); hashParts.push(light.type); hashParts.push((light.space === "world") ? "w" : "v"); if (light.castsShadow) { hashParts.push("sh"); } } if (this.lightMaps.length > 0) { hashParts.push("/lm"); } if (this.reflectionMaps.length > 0) { hashParts.push("/rm"); } hashParts.push(";"); hash = hashParts.join(""); return hash; }; this.addLight = function (state) { this.lights.push(state); ambientLight = null; hash = null; }; this.removeLight = function (state) { for (let i = 0, len = this.lights.length; i < len; i++) { const light = this.lights[i]; if (light.id === state.id) { this.lights.splice(i, 1); if (ambientLight && ambientLight.id === state.id) { ambientLight = null; } hash = null; return; } } }; this.addReflectionMap = function (state) { this.reflectionMaps.push(state); hash = null; }; this.removeReflectionMap = function (state) { for (let i = 0, len = this.reflectionMaps.length; i < len; i++) { if (this.reflectionMaps[i].id === state.id) { this.reflectionMaps.splice(i, 1); hash = null; return; } } }; this.addLightMap = function (state) { this.lightMaps.push(state); hash = null; }; this.removeLightMap = function (state) { for (let i = 0, len = this.lightMaps.length; i < len; i++) { if (this.lightMaps[i].id === state.id) { this.lightMaps.splice(i, 1); hash = null; return; } } }; this.getAmbientColorAndIntensity = function () { if (!ambientLight) { for (let i = 0, len = this.lights.length; i < len; i++) { const light = this.lights[i]; if (light.type === "ambient") { ambientLight = light; break; } } } if (ambientLight) { const color = ambientLight.color; const intensity = ambientLight.intensity; ambientColorIntensity[0] = color[0]; ambientColorIntensity[1] = color[1]; ambientColorIntensity[2] = color[2]; ambientColorIntensity[3] = intensity; return ambientColorIntensity; } else { return DEFAULT_AMBIENT; } }; })(); /** * Publishes input events that occur on this Scene's canvas. * * @property input * @type {Input} * @final */ this.input = new Input(this, { dontClear: true, // Never destroy this component with Scene#clear(); element: this.canvas.canvas, keyboardEventsElement: cfg.keyboardEventsElement }); /** * Configures this Scene's units of measurement and coordinate mapping between Real-space and World-space 3D coordinate systems. * * @property metrics * @type {Metrics} * @final */ this.metrics = new Metrics(this, { units: cfg.units, scale: cfg.scale, origin: cfg.origin }); /** Configures Scalable Ambient Obscurance (SAO) for this Scene. * @type {SAO} * @final */ this.sao = new SAO(this, { enabled: cfg.saoEnabled }); /** Configures Cross Sections for this Scene. * @type {CrossSections} * @final */ this.crossSections = new CrossSections(this, { }); this.ticksPerRender = cfg.ticksPerRender; this.ticksPerOcclusionTest = cfg.ticksPerOcclusionTest; this.passes = cfg.passes; this.clearEachPass = cfg.clearEachPass; this.gammaInput = cfg.gammaInput; this.gammaOutput = cfg.gammaOutput; this.gammaFactor = cfg.gammaFactor; this._entityOffsetsEnabled = !!cfg.entityOffsetsEnabled; this._logarithmicDepthBufferEnabled = !!cfg.logarithmicDepthBufferEnabled; this._dtxEnabled = (cfg.dtxEnabled !== false); this._pbrEnabled = !!cfg.pbrEnabled; this._colorTextureEnabled = (cfg.colorTextureEnabled !== false); this._dtxEnabled = !!cfg.dtxEnabled; this._markerZOffset = cfg.markerZOffset; // Register Scene on xeokit // Do this BEFORE we add components below core._addScene(this); this._initDefaults(); // Global components this._viewport = new Viewport(this, { id: "default.viewport", autoBoundary: true, dontClear: true // Never destroy this component with Scene#clear(); }); this._camera = new Camera(this, { id: "default.camera", dontClear: true // Never destroy this component with Scene#clear(); }); this._sectionCaps = new SectionCaps(this); // Default lights new AmbientLight(this, { color: [1.0, 1.0, 1.0], intensity: 0.7 }); new DirLight(this, { dir: [0.8, -.5, -0.5], color: [0.67, 0.67, 1.0], intensity: 0.7, space: "world" }); new DirLight(this, { dir: [-0.8, -1.0, 0.5], color: [1, 1, .9], intensity: 0.9, space: "world" }); this._camera.on("dirty", () => { this._renderer.imageDirty(); }); } _initDefaults() { } _addComponent(component) { if (component.id) { // Manual ID if (this.components[component.id]) { this.error("Component " + utils.inQuotes(component.id) + " already exists in Scene - ignoring ID, will randomly-generate instead"); component.id = null; } } if (!component.id) { // Auto ID if (window.nextID === undefined) { window.nextID = 0; } //component.id = math.createUUID(); component.id = "__" + window.nextID++; while (this.components[component.id]) { component.id = math.createUUID(); } } this.components[component.id] = component; // Register for class type const type = component.type; let types = this.types[component.type]; if (!types) { types = this.types[type] = {}; } types[component.id] = component; if (component.compile) { this._compilables[component.id] = component; } if (component.isDrawable) { this._renderer.addDrawable(component.id, component); this._collidables[component.id] = component; } } _removeComponent(component) { var id = component.id; var type = component.type; delete this.components[id]; // Unregister for types const types = this.types[type]; if (types) { delete types[id]; if (utils.isEmptyObject(types)) { delete this.types[type]; } } if (component.compile) { delete this._compilables[component.id]; } if (component.isDrawable) { this._renderer.removeDrawable(component.id); delete this._collidables[component.id]; } } // Methods below are called by various component types to register themselves on their // Scene. Violates Hollywood Principle, where we could just filter on type in _addComponent, // but this is faster than checking the type of each component in such a filter. _capMaterialUpdated(entityId, modelId) { this._sectionCaps._onCapMaterialUpdated(entityId, modelId); } _sectionPlaneCreated(sectionPlane) { this.sectionPlanes[sectionPlane.id] = sectionPlane; this.scene._sectionPlanesState.addSectionPlane(sectionPlane._state); this.scene.fire("sectionPlaneCreated", sectionPlane, true /* Don't retain event */); this._needRecompile = true; } _bitmapCreated(bitmap) { this.bitmaps[bitmap.id] = bitmap; this.scene.fire("bitmapCreated", bitmap, true /* Don't retain event */); } _lineSetCreated(lineSet) { this.lineSets[lineSet.id] = lineSet; this.scene.fire("lineSetCreated", lineSet, true /* Don't retain event */); } _lightCreated(light) { this.lights[light.id] = light; this.scene._lightsState.addLight(light._state); this._needRecompile = true; } _lightMapCreated(lightMap) { this.lightMaps[lightMap.id] = lightMap; this.scene._lightsState.addLightMap(lightMap._state); this._needRecompile = true; } _reflectionMapCreated(reflectionMap) { this.reflectionMaps[reflectionMap.id] = reflectionMap; this.scene._lightsState.addReflectionMap(reflectionMap._state); this._needRecompile = true; } _sectionPlaneDestroyed(sectionPlane) { delete this.sectionPlanes[sectionPlane.id]; this.scene._sectionPlanesState.removeSectionPlane(sectionPlane._state); this.scene.fire("sectionPlaneDestroyed", sectionPlane, true /* Don't retain event */); this._needRecompile = true; } _bitmapDestroyed(bitmap) { delete this.bitmaps[bitmap.id]; this.scene.fire("bitmapDestroyed", bitmap, true /* Don't retain event */); } _lineSetDestroyed(lineSet) { delete this.lineSets[lineSet.id]; this.scene.fire("lineSetDestroyed", lineSet, true /* Don't retain event */); } _lightDestroyed(light) { delete this.lights[light.id]; this.scene._lightsState.removeLight(light._state); this._needRecompile = true; } _lightMapDestroyed(lightMap) { delete this.lightMaps[lightMap.id]; this.scene._lightsState.removeLightMap(lightMap._state); this._needRecompile = true; } _reflectionMapDestroyed(reflectionMap) { delete this.reflectionMaps[reflectionMap.id]; this.scene._lightsState.removeReflectionMap(reflectionMap._state); this._needRecompile = true; } _registerModel(entity) { this.models[entity.id] = entity; this._modelIds = null; // Lazy regenerate } _deregisterModel(entity) { const modelId = entity.id; delete this.models[modelId]; this._modelIds = null; // Lazy regenerate this.fire("modelUnloaded", modelId); } _registerObject(entity) { this.objects[entity.id] = entity; this._numObjects++; this._objectIds = null; // Lazy regenerate } _deregisterObject(entity) { delete this.objects[entity.id]; this._numObjects--; this._objectIds = null; // Lazy regenerate } _objectVisibilityUpdated(entity, notify = true) { if (entity.visible) { this.visibleObjects[entity.id] = entity; this._numVisibleObjects++; } else { delete this.visibleObjects[entity.id]; this._numVisibleObjects--; } this._visibleObjectIds = null; // Lazy regenerate if (notify) { this.fire("objectVisibility", entity, true); } } _deRegisterVisibleObject(entity) { delete this.visibleObjects[entity.id]; this._numVisibleObjects--; this._visibleObjectIds = null; // Lazy regenerate } _objectXRayedUpdated(entity, notify = true) { if (entity.xrayed) { this.xrayedObjects[entity.id] = entity; this._numXRayedObjects++; } else { delete this.xrayedObjects[entity.id]; this._numXRayedObjects--; } this._xrayedObjectIds = null; // Lazy regenerate if (notify) { this.fire("objectXRayed", entity, true); } } _deRegisterXRayedObject(entity) { delete this.xrayedObjects[entity.id]; this._numXRayedObjects--; this._xrayedObjectIds = null; // Lazy regenerate } _objectHighlightedUpdated(entity) { if (entity.highlighted) { this.highlightedObjects[entity.id] = entity; this._numHighlightedObjects++; } else { delete this.highlightedObjects[entity.id]; this._numHighlightedObjects--; } this._highlightedObjectIds = null; // Lazy regenerate } _deRegisterHighlightedObject(entity) { delete this.highlightedObjects[entity.id]; this._numHighlightedObjects--; this._highlightedObjectIds = null; // Lazy regenerate } _objectSelectedUpdated(entity, notify = true) { if (entity.selected) { this.selectedObjects[entity.id] = entity; this._numSelectedObjects++; } else { delete this.selectedObjects[entity.id]; this._numSelectedObjects--; } this._selectedObjectIds = null; // Lazy regenerate if (notify) { this.fire("objectSelected", entity, true); } } _deRegisterSelectedObject(entity) { delete this.selectedObjects[entity.id]; this._numSelectedObjects--; this._selectedObjectIds = null; // Lazy regenerate } _objectColorizeUpdated(entity, colorized) { if (colorized) { this.colorizedObjects[entity.id] = entity; this._numColorizedObjects++; } else { delete this.colorizedObjects[entity.id]; this._numColorizedObjects--; } this._colorizedObjectIds = null; // Lazy regenerate } _deRegisterColorizedObject(entity) { delete this.colorizedObjects[entity.id]; this._numColorizedObjects--; this._colorizedObjectIds = null; // Lazy regenerate } _objectOpacityUpdated(entity, opacityUpdated) { if (opacityUpdated) { this.opacityObjects[entity.id] = entity; this._numOpacityObjects++; } else { delete this.opacityObjects[entity.id]; this._numOpacityObjects--; } this._opacityObjectIds = null; // Lazy regenerate } _deRegisterOpacityObject(entity) { delete this.opacityObjects[entity.id]; this._numOpacityObjects--; this._opacityObjectIds = null; // Lazy regenerate } _objectOffsetUpdated(entity, offset) { if (!offset || offset[0] === 0 && offset[1] === 0 && offset[2] === 0) { this.offsetObjects[entity.id] = entity; this._numOffsetObjects++; } else { delete this.offsetObjects[entity.id]; this._numOffsetObjects--; } this._offsetObjectIds = null; // Lazy regenerate } _deRegisterOffsetObject(entity) { delete this.offsetObjects[entity.id]; this._numOffsetObjects--; this._offsetObjectIds = null; // Lazy regenerate } _webglContextLost() { // this.loading++; this.canvas.spinner.processes++; for (const id in this.components) { if (this.components.hasOwnProperty(id)) { const component = this.components[id]; if (component._webglContextLost) { component._webglContextLost(); } } } this._renderer.webglContextLost(); } _webglContextRestored() { const gl = this.canvas.gl; for (const id in this.components) { if (this.components.hasOwnProperty(id)) { const component = this.components[id]; if (component._webglContextRestored) { component._webglContextRestored(gl); } } } this._renderer.webglContextRestored(gl); //this.loading--; this.canvas.spinner.processes--; } /** * Returns the capabilities of this Scene. * * @private * @returns {{astcSupported: boolean, etc1Supported: boolean, pvrtcSupported: boolean, etc2Supported: boolean, dxtSupported: boolean, bptcSupported: boolean}} */ get capabilities() { return this._renderer.capabilities; } /** * Whether {@link Entity#offset} is enabled. * * This is set via the {@link Viewer} constructor and is ````false```` by default. * * @returns {Boolean} True if {@link Entity#offset} is enabled. */ get entityOffsetsEnabled() { return this._entityOffsetsEnabled; } /** * Whether geometry is readable. * * This is set via the {@link Viewer} constructor and is ````false```` by default. * * The ````readableGeometryEnabled```` option for ````Scene#pick```` only works if this is set ````true````. * * Note that when ````true````, this configuration will increase the amount of browser memory used by the Viewer. * * @returns {Boolean} True if geometry is readable. */ get readableGeometryEnabled() { return this._readableGeometry; } /** * Whether precision surface picking is enabled. * @deprecated * @returns {*|boolean} */ get pickSurfacePrecisionEnabled() { return this._readableGeometry; } /** * Whether logarithmic depth buffer is enabled. * * This is set via the {@link Viewer} constructor and is ````false```` by default. * * @returns {Boolean} True if logarithmic depth buffer is enabled. */ get logarithmicDepthBufferEnabled() { return this._logarithmicDepthBufferEnabled; } /** * Sets the number of {@link SectionPlane}s for which this Scene pre-caches resources. * * This property enhances the efficiency of SectionPlane creation by proactively allocating and caching Viewer resources for a specified quantity * of SectionPlanes. Introducing this parameter streamlines the initial creation speed of SectionPlanes, particularly up to the designated quantity. This parameter internally * configures renderer logic for the specified number of SectionPlanes, eliminating the need for setting up logic with each SectionPlane creation and thereby enhancing * responsiveness. It is important to consider that each SectionPlane impacts rendering performance, so it is recommended to set this value to a quantity that aligns with * your expected usage. * * Default is ````0````. */ set numCachedSectionPlanes(numCachedSectionPlanes) { numCachedSectionPlanes = numCachedSectionPlanes || 0; if (this._sectionPlanesState.getNumCachedSectionPlanes() !== numCachedSectionPlanes) { this._sectionPlanesState.setNumCachedSectionPlanes(numCachedSectionPlanes); this._needRecompile = true; this.glRedraw(); } } /** * Gets the number of {@link SectionPlane}s for which this Scene pre-caches resources. * * This property enhances the efficiency of SectionPlane creation by proactively allocating and caching Viewer resources for a specified quantity * of SectionPlanes. Introducing this parameter streamlines the initial creation speed of SectionPlanes, particularly up to the designated quantity. This parameter internally * configures renderer logic for the specified number of SectionPlanes, eliminating the need for setting up logic with each SectionPlane creation and thereby enhancing * responsiveness. It is important to consider that each SectionPlane impacts rendering performance, so it is recommended to set this value to a quantity that aligns with * your expected usage. * * Default is ````0````. * * @returns {number} The number of {@link SectionPlane}s for which this Scene pre-caches resources. */ get numCachedSectionPlanes() { return this._sectionPlanesState.getNumCachedSectionPlanes(); } /** * Sets whether physically-based rendering is enabled. * * Default is ````false````. */ set pbrEnabled(pbrEnabled) { this._pbrEnabled = !!pbrEnabled; this.glRedraw(); } /** * Gets whether physically-based rendering is enabled. * * Default is ````false````. * * @returns {Boolean} True if quality rendering is enabled. */ get pbrEnabled() { return this._pbrEnabled; } /** * Sets whether data texture scene representation (DTX) is enabled for the {@link Scene}. * * Even when enabled, DTX will only work if supported. * * Default value is ````false````. * * @type {Boolean} */ set dtxEnabled(value) { value = !!value; if (this._dtxEnabled === value) { return; } this._dtxEnabled = value; } /** * Gets whether data texture-based scene representation (DTX) is enabled for the {@link Scene}. * * Even when enabled, DTX will only apply if supported. * * Default value is ````false````. * * @type {Boolean} */ get dtxEnabled() { return this._dtxEnabled; } /** * Sets whether basic color texture rendering is enabled. * * Default is ````true````. * * @returns {Boolean} True if basic color texture rendering is enabled. */ set colorTextureEnabled(colorTextureEnabled) { this._colorTextureEnabled = !!colorTextureEnabled; this.glRedraw(); } /** * Gets whether basic color texture rendering is enabled. * * Default is ````true````. * * @returns {Boolean} True if basic color texture rendering is enabled. */ get colorTextureEnabled() { return this._colorTextureEnabled; } /** * Gets the Z value of offset for Marker's OcclusionTester. * The closest the value is to 0.000 the more precise OcclusionTester will be, but at the same time the less * precise it will behave for Markers that are located exactly on the Surface. * * Default is ````-0.001````. * * @returns {Number} Z offset for Marker */ get markerZOffset() { if (this._markerZOffset == null) { return -0.001; } return this._markerZOffset; } /** * Performs an occlusion test on all {@link Marker}s in this {@link Scene}. * * Sets each {@link Marker#visible} ````true```` if the Marker is currently not occluded by any opaque {@link Entity}s * in the Scene, or ````false```` if an Entity is occluding it. */ doOcclusionTest() { if (this._needRecompile) { this._recompile(); this._needRecompile = false; } this._renderer.doOcclusionTest(); } /** * Renders a single frame of this Scene. * * The Scene will periodically render itself after any updates, but you can call this method to force a render * if required. * * @param {Boolean} [forceRender=false] Forces a render when true, otherwise only renders if something has changed in this Scene * since the last render. */ render(forceRender) { if (forceRender) { core.runTasks(); } const renderEvent = { sceneId: null, pass: 0 }; if (this._needRecompile) { this._recompile(); this._renderer.imageDirty(); this._needRecompile = false; } if (!forceRender && !this._renderer.needsRender()) { return; } renderEvent.sceneId = this.id; const passes = this._passes; const clearEachPass = this._clearEachPass; let pass; let clear; for (pass = 0; pass < passes; pass++) { renderEvent.pass = pass; /** * Fired when about to render a frame for a Scene. * * @event rendering * @param {String} sceneID The ID of this Scene. * @param {Number} pass Index of the pass we are about to render (see {@link Scene#passes}). */ this.fire("rendering", renderEvent, true); clear = clearEachPass || (pass === 0); this._renderer.render({pass: pass, clear: clear, force: forceRender}); /** * Fired when we have just rendered a frame for a Scene. * * @event rendering * @param {String} sceneID The ID of this Scene. * @param {Number} pass Index of the pass we rendered (see {@link Scene#passes}). */ this.fire("rendered", renderEvent, true); } this._saveAmbientColor(); } /** * @private */ compile() { if (this._needRecompile) { this._recompile(); this._renderer.imageDirty(); this._needRecompile = false; } } _recompile() { for (const id in this._compilables) { if (this._compilables.hasOwnProperty(id)) { this._compilables[id].compile(); } } this._renderer.shadowsDirty(); this.fire("compile", this, true); } _saveAmbientColor() { const canvas = this.canvas; if (!canvas.transparent && !canvas.backgroundImage && !canvas.backgroundColor) { const ambientColorIntensity = this._lightsState.getAmbientColorAndIntensity(); if (!this._lastAmbientColor || this._lastAmbientColor[0] !== ambientColorIntensity[0] || this._lastAmbientColor[1] !== ambientColorIntensity[1] || this._lastAmbientColor[2] !== ambientColorIntensity[2] || this._lastAmbientColor[3] !== ambientColorIntensity[3]) { canvas.backgroundColor = ambientColorIntensity; if (!this._lastAmbientColor) { this._lastAmbientColor = math.vec4([0, 0, 0, 1]); } this._lastAmbientColor.set(ambientColorIntensity); } } else { this._lastAmbientColor = null; } } /** * Gets the IDs of the {@link Entity}s in {@link Scene#models}. * * @type {String[]} */ get modelIds() { if (!this._modelIds) { this._modelIds = Object.keys(this.models); } return this._modelIds; } /** * Gets the number of {@link Entity}s in {@link Scene#objects}. * * @type {Number} */ get numObjects() { return this._numObjects; } /** * Gets the IDs of the {@link Entity}s in {@link Scene#objects}. * * @type {String[]} */ get objectIds() { if (!this._objectIds) { this._objectIds = Object.keys(this.objects); } return this._objectIds; } /** * Gets the number of {@link Entity}s in {@link Scene#visibleObjects}. * * @type {Number} */ get numVisibleObjects() { return this._numVisibleObjects; } /** * Gets the IDs of the {@link Entity}s in {@link Scene#visibleObjects}. * * @type {String[]} */ get visibleObjectIds() { if (!this._visibleObjectIds) { this._visibleObjectIds = Object.keys(this.visibleObjects); } return this._visibleObjectIds; } /** * Gets the number of {@link Entity}s in {@link Scene#xrayedObjects}. * * @type {Number} */ get numXRayedObjects() { return this._numXRayedObjects; } /** * Gets the IDs of the {@link Entity}s in {@link Scene#xrayedObjects}. * * @type {String[]} */ get xrayedObjectIds() { if (!this._xrayedObjectIds) { this._xrayedObjectIds = Object.keys(this.xrayedObjects); } return this._xrayedObjectIds; } /** * Gets the number of {@link Entity}s in {@link Scene#highlightedObjects}. * * @type {Number} */ get numHighlightedObjects() { return this._numHighlightedObjects; } /** * Gets the IDs of the {@link Entity}s in {@link Scene#highlightedObjects}. * * @type {String[]} */ get highlightedObjectIds() { if (!this._highlightedObjectIds) { this._highlightedObjectIds = Object.keys(this.highlightedObjects); } return this._highlightedObjectIds; } /** * Gets the number of {@link Entity}s in {@link Scene#selectedObjects}. * * @type {Number} */ get numSelectedObjects() { return this._numSelectedObjects; } /** * Gets the IDs of the {@link Entity}s in {@link Scene#selectedObjects}. * * @type {String[]} */ get selectedObjectIds() { if (!this._selectedObjectIds) { this._selectedObjectIds = Object.keys(this.selectedObjects); } return this._selectedObjectIds; } /** * Gets the number of {@link Entity}s in {@link Scene#colorizedObjects}. * * @type {Number} */ get numColorizedObjects() { return this._numColorizedObjects; } /** * Gets the IDs of the {@link Entity}s in {@link Scene#colorizedObjects}. * * @type {String[]} */ get colorizedObjectIds() { if (!this._colorizedObjectIds) { this._colorizedObjectIds = Object.keys(this.colorizedObjects); } return this._colorizedObjectIds; } /** * Gets the IDs of the {@link Entity}s in {@link Scene#opacityObjects}. * * @type {String[]} */ get opacityObjectIds() { if (!this._opacityObjectIds) { this._opacityObjectIds = Object.keys(this.opacityObjects); } return this._opacityObjectIds; } /** * Gets the IDs of the {@link Entity}s in {@link Scene#offsetObjects}. * * @type {String[]} */ get offsetObjectIds() { if (!this._offsetObjectIds) { this._offsetObjectIds = Object.keys(this.offsetObjects); } return this._offsetObjectIds; } /** * Sets the number of "ticks" that happen between each render or this Scene. * * Default value is ````1````. * * @type {Number} */ set ticksPerRender(value) { if (value === undefined || value === null) { value = 1; } else if (!utils.isNumeric(value) || value <= 0) { this.error("Unsupported value for 'ticksPerRender': '" + value + "' - should be an integer greater than zero."); value = 1; } if (value === this._ticksPerRender) { return; } this._ticksPerRender = value; } /** * Gets the number of "ticks" that happen between each render or this Scene. * * Default value is ````1````. * * @type {Number} */ get ticksPerRender() { return this._ticksPerRender; } /** * Sets the number of "ticks" that happen between occlusion testing for {@link Marker}s. * * Default value is ````20````. * * @type {Number} */ set ticksPerOcclusionTest(value) { if (value === undefined || value === null) { value = 20; } else if (!utils.isNumeric(value) || value <= 0) { this.error("Unsupported value for 'ticksPerOcclusionTest': '" + value + "' - should be an integer greater than zero."); value = 20; } if (value === this._ticksPerOcclusionTest) { return; } this._ticksPerOcclusionTest = value; } /** * Gets the number of "ticks" that happen between each render of this Scene. * * Default value is ````1````. * * @type {Number} */ get ticksPerOcclusionTest() { return this._ticksPerOcclusionTest; } /** * Sets the number of times this Scene renders per frame. * * Default value is ````1````. * * @type {Number} */ set passes(value) { if (value === undefined || value === null) { value = 1; } else if (!utils.isNumeric(value) || value <= 0) { this.error("Unsupported value for 'passes': '" + value + "' - should be an integer greater than zero."); value = 1; } if (value === this._passes) { return; } this._passes = value; this.glRedraw(); } /** * Gets the number of times this Scene renders per frame. * * Default value is ````1````. * * @type {Number} */ get passes() { return this._passes; } /** * When {@link Scene#passes} is greater than ````1````, indicates whether or not to clear the canvas before each pass (````true````) or just before the first pass (````false````). * * Default value is ````false````. * * @type {Boolean} */ set clearEachPass(value) { value = !!value; if (value === this._clearEachPass) { return; } this._clearEachPass = value; this.glRedraw(); } /** * When {@link Scene#passes} is greater than ````1````, indicates whether or not to clear the canvas before each pass (````true````) or just before the first pass (````false````). * * Default value is ````false````. * * @type {Boolean} */ get clearEachPass() { return this._clearEachPass; } /** * Sets whether or not {@link Scene} should expect all {@link Texture}s and colors to have pre-multiplied gamma. * * Default value is ````false````. * * @type {Boolean} */ set gammaInput(value) { value = value !== false; if (value === this._renderer.gammaInput) { return; } this._renderer.gammaInput = value; this._needRecompile = true; this.glRedraw(); } /** * Gets whether or not {@link Scene} should expect all {@link Texture}s and colors to have pre-multiplied gamma. * * Default value is ````false````. * * @type {Boolean} */ get gammaInput() { return this._renderer.gammaInput; } /** * Sets whether or not to render pixels with pre-multiplied gama. * * Default value is ````false````. * * @type {Boolean} */ set gammaOutput(value) { value = !!value; if (value === this._renderer.gammaOutput) { return; } this._renderer.gammaOutput = value; this._needRecompile = true; this.glRedraw(); } /** * Gets whether or not to render pixels with pre-multiplied gama. * * Default value is ````true````. * * @type {Boolean} */ get gammaOutput() { return this._renderer.gammaOutput; } /** * Sets the gamma factor to use when {@link Scene#gammaOutput} is set true. * * Default value is ````2.2````. * * @type {Number} */ set gammaFactor(value) { value = (value === undefined || value === null) ? 2.2 : value; if (value === this._renderer.gammaFactor) { return; } this._renderer.gammaFactor = value; this.glRedraw(); } /** * Gets the gamma factor to use when {@link Scene#gammaOutput} is set true. * * Default value is ````2.2````. * * @type {Number} */ get gammaFactor() { return this._renderer.gammaFactor; } /** * Gets the default {@link Geometry} for this Scene, which is a {@link ReadableGeometry} with a unit-sized box shape. * * Has {@link ReadableGeometry#id} set to "default.geometry". * * {@link Mesh}s in this Scene have {@link Mesh#geometry} set to this {@link ReadableGeometry} by default. * * @type {ReadableGeometry} */ get geometry() { return this.components["default.geometry"] || buildBoxGeometry(ReadableGeometry); } /** * Gets the default {@link Material} for this Scene, which is a {@link PhongMaterial}. * * Has {@link PhongMaterial#id} set to "default.material". * * {@link Mesh}s in this Scene have {@link Mesh#material} set to this {@link PhongMaterial} by default. * * @type {PhongMaterial} */ get material() { return this.components["default.material"] || new PhongMaterial(this, { id: "default.material", emissive: [0.4, 0.4, 0.4], // Visible by default on geometry without normals dontClear: true }); } /** * Gets the default xraying {@link EmphasisMaterial} for this Scene. * * Has {@link EmphasisMaterial#id} set to "default.xrayMaterial". * * {@link Mesh}s in this Scene have {@link Mesh#xrayMaterial} set to this {@link EmphasisMaterial} by default. * * {@link Mesh}s are xrayed while {@link Mesh#xrayed} is ````true````. * * @type {EmphasisMaterial} */ get xrayMaterial() { return this.components["default.xrayMaterial"] || new EmphasisMaterial(this, { id: "default.xrayMaterial", fill: true, fillColor: [0.970588207244873, 0.7965892553329468, 0.6660899519920349], fillAlpha: 0.4, edges: true, edgeColor: [0.529411792755127, 0.4577854573726654, 0.4100345969200134], edgeAlpha: 1.0, edgeWidth: 1, dontClear: true }); } /** * Gets the default highlight {@link EmphasisMaterial} for this Scene. * * Has {@link EmphasisMaterial#id} set to "default.highlightMaterial". * * {@link Mesh}s in this Scene have {@link Mesh#highlightMaterial} set to this {@link EmphasisMaterial} by default. * * {@link Mesh}s are highlighted while {@link Mesh#highlighted} is ````true````. * * @type {EmphasisMaterial} */ get highlightMaterial() { return this.components["default.highlightMaterial"] || new EmphasisMaterial(this, { id: "default.highlightMaterial", fill: true, fillColor: [1.0, 1.0, 0.0], fillAlpha: 0.5, edges: true, edgeColor: [1.0, 1.0, 1.0], edgeAlpha: 1.0, edgeWidth: 1, dontClear: true }); } /** * Gets the default selection {@link EmphasisMaterial} for this Scene. * * Has {@link EmphasisMaterial#id} set to "default.selectedMaterial". * * {@link Mesh}s in this Scene have {@link Mesh#highlightMaterial} set to this {@link EmphasisMaterial} by default. * * {@link Mesh}s are highlighted while {@link Mesh#highlighted} is ````true````. * * @type {EmphasisMaterial} */ get selectedMaterial() { return this.components["default.selectedMaterial"] || new EmphasisMaterial(this, { id: "default.selectedMaterial", fill: true, fillColor: [0.0, 1.0, 0.0], fillAlpha: 0.5, edges: true, edgeColor: [1.0, 1.0, 1.0], edgeAlpha: 1.0, edgeWidth: 1, dontClear: true }); } /** * Gets the default {@link EdgeMaterial} for this Scene. * * Has {@link EdgeMaterial#id} set to "default.edgeMaterial". * * {@link Mesh}s in this Scene have {@link Mesh#edgeMaterial} set to this {@link EdgeMaterial} by default. * * {@link Mesh}s have their edges emphasized while {@link Mesh#edges} is ````true````. * * @type {EdgeMaterial} */ get edgeMaterial() { return this.components["default.edgeMaterial"] || new EdgeMaterial(this, { id: "default.edgeMaterial", fill: true, fillColor: [0.4, 0.4, 0.4], fillAlpha: 0.2, edges: true, edgeColor: [0.0, 0.0, 0.0], edgeAlpha: 1.0, edgeWidth: 1, dontClear: true }); } /** * Gets the {@link PointsMaterial} for this Scene. * * @type {PointsMaterial} */ get pointsMaterial() { return this.components["default.pointsMaterial"] || new PointsMaterial(this, { id: "default.pointsMaterial", fill: true, fillColor: [0.4, 0.4, 0.4], fillAlpha: 0.2, edges: true, edgeColor: [0.2, 0.2, 0.2], edgeAlpha: 0.5, edgeWidth: 1, dontClear: true }); } /** * Gets the {@link LinesMaterial} for this Scene. * * @type {LinesMaterial} */ get linesMaterial() { return this.components["default.linesMaterial"] || new LinesMaterial(this, { id: "default.linesMaterial", fill: true, fillColor: [0.4, 0.4, 0.4], fillAlpha: 0.2, edges: true, edgeColor: [0.2, 0.2, 0.2], edgeAlpha: 0.5, edgeWidth: 1, dontClear: true }); } /** * Gets the {@link Viewport} for this Scene. * * @type Viewport */ get viewport() { return this._viewport; } /** * Gets the {@link Camera} for this Scene. * * @type {Camera} */ get camera() { return this._camera; } /** * Gets the World-space 3D center of this Scene. * *@type {Number[]} */ get center() { if (this._aabbDirty || !this._center) { const aabb = this.aabb; if (!this._center) { this._center = math.vec3(); } this._center[0] = (aabb[0] + aabb[3]) / 2; this._center[1] = (aabb[1] + aabb[4]) / 2; this._center[2] = (aabb[2] + aabb[5]) / 2; } return this._center; } /** * Gets the World-space axis-aligned 3D boundary (AABB) of this Scene. * * The AABB is represented by a six-element Float64Array containing the min/max extents of the axis-aligned volume, ie. ````[xmin, ymin,zmin,xmax,ymax, zmax]````. * * When the Scene has no content, will be ````[-100,-100,-100,100,100,100]````. * * @type {Number[]} */ get aabb() { if (this._aabbDirty) { if (!this._aabb) { this._aabb = math.AABB3(); } let xmin = math.MAX_DOUBLE; let ymin = math.MAX_DOUBLE; let zmin = math.MAX_DOUBLE; let xmax = math.MIN_DOUBLE; let ymax = math.MIN_DOUBLE; let zmax = math.MIN_DOUBLE; let aabb; const collidables = this._collidables; let collidable; let valid = false; for (const collidableId in collidables) { if (collidables.hasOwnProperty(collidableId)) { collidable = collidables[collidableId]; if (collidable.collidable === false) { continue; } aabb = collidable.aabb; if (aabb[0] < xmin) { xmin = aabb[0]; } if (aabb[1] < ymin) { ymin = aabb[1]; } if (aabb[2] < zmin) { zmin = aabb[2]; } if (aabb[3] > xmax) { xmax = aabb[3]; } if (aabb[4] > ymax) { ymax = aabb[4]; } if (aabb[5] > zmax) { zmax = aabb[5]; } valid = true; } } if (!valid) { xmin = -100; ymin = -100; zmin = -100; xmax = 100; ymax = 100; zmax = 100; } this._aabb[0] = xmin; this._aabb[1] = ymin; this._aabb[2] = zmin; this._aabb[3] = xmax; this._aabb[4] = ymax; this._aabb[5] = zmax; this._aabbDirty = false; this._center = null; } return this._aabb; } _setAABBDirty() { //if (!this._aabbDirty) { this._aabbDirty = true; this.fire("boundary"); // } } /** * Attempts to pick an {@link Entity} in this Scene. * * Ignores {@link Entity}s with {@link Entity#pickable} set ````false````. * * When an {@link Entity} is picked, fires a "pick" event on the {@link Entity} with the pick result as parameters. * * Picking the {@link Entity} at the given canvas coordinates: * ````javascript * var pickResult = scene.pick({ * canvasPos: [23, 131] * }); * * if (pickResult) { // Picked an Entity * var entity = pickResult.entity; * } * ```` * * Picking, with a ray cast through the canvas, hits an {@link Entity}: * * ````javascript * var pickResult = scene.pick({ * pickSurface: true, * canvasPos: [23, 131] * }); * * if (pickResult) { // Picked an Entity * * var entity = pickResult.entity; * * if (pickResult.primitive === "triangle") { * * // Picked a triangle on the entity surface * * var primIndex = pickResult.primIndex; // Position of triangle's first index in the picked Entity's Geometry's indices array * var indices = pickResult.indices; // UInt32Array containing the triangle's vertex indices * var localPos = pickResult.localPos; // Float64Array containing the picked Local-space position on the triangle * var worldPos = pickResult.worldPos; // Float64Array containing the picked World-space position on the triangle * var viewPos = pickResult.viewPos; // Float64Array containing the picked View-space position on the triangle * var bary = pickResult.bary; // Float64Array containing the picked barycentric position within the triangle * var worldNormal = pickResult.worldNormal; // Float64Array containing the interpolated World-space normal vector at the picked position on the triangle * var uv = pickResult.uv; // Float64Array containing the interpolated UV coordinates at the picked position on the triangle * * } else if (pickResult.worldPos && pickResult.worldNormal) { * * // Picked a point and normal on the entity surface * * var worldPos = pickResult.worldPos; // Float64Array containing the picked World-space position on the Entity surface * var worldNormal = pickResult.worldNormal; // Float64Array containing the picked World-space normal vector on the Entity Surface * } * } * ```` * * Picking the {@link Entity} that intersects an arbitrarily-aligned World-space ray: * * ````javascript * var pickResult = scene.pick({ * pickSurface: true, // Picking with arbitrarily-positioned ray * origin: [0,0,-5], // Ray origin * direction: [0,0,1] // Ray direction * }); * * if (pickResult) { // Picked an Entity with the ray * * var entity = pickResult.entity; * * if (pickResult.primitive == "triangle") { * * // Picked a triangle on the entity surface * * var primitive = pickResult.primitive; // Type of primitive that was picked, usually "triangles" * var primIndex = pickResult.primIndex; // Position of triangle's first index in the picked Entity's Geometry's indices array * var indices = pickResult.indices; // UInt32Array containing the triangle's vertex indices * var localPos = pickResult.localPos; // Float64Array containing the picked Local-space position on the triangle * var worldPos = pickResult.worldPos; // Float64Array containing the picked World-space position on the triangle * var viewPos = pickResult.viewPos; // Float64Array containing the picked View-space position on the triangle * var bary = pickResult.bary; // Float64Array containing the picked barycentric position within the triangle * var worldNormal = pickResult.worldNormal; // Float64Array containing the interpolated World-space normal vector at the picked position on the triangle * var uv = pickResult.uv; // Float64Array containing the interpolated UV coordinates at the picked position on the triangle * var origin = pickResult.origin; // Float64Array containing the World-space ray origin * var direction = pickResult.direction; // Float64Array containing the World-space ray direction * * } else if (pickResult.worldPos && pickResult.worldNormal) { * * // Picked a point and normal on the entity surface * * var worldPos = pickResult.worldPos; // Float64Array containing the picked World-space position on the Entity surface * var worldNormal = pickResult.worldNormal; // Float64Array containing the picked World-space normal vector on the Entity Surface * } * } * ```` * * @param {*} params Picking parameters. * @param {Boolean} [params.pickSurface=false] Whether to find the picked position on the surface of the Entity. * @param {Boolean} [params.pickSurfacePrecision=false] When picking an Entity surface position, indicates whether or not we want full-precision {@link PickResult#worldPos}. Only works when {@link Scene#readableGeometryEnabled} is ````true````. If pick succeeds, the returned {@link PickResult} will have {@link PickResult#precision} set ````true````, to indicate that it contains full-precision surface pick results. * @param {Boolean} [params.pickSurfaceNormal=false] Whether to find the picked normal on the surface of the Entity. Only works if ````pickSurface```` is given. * @param {Number[]} [params.canvasPos] Canvas-space coordinates. When ray-picking, this will override the **origin** and ** direction** parameters and will cause the ray to be fired through the canvas at this position, directly along the negative View-space Z-axis. * @param {Number[]} [params.origin] World-space ray origin when ray-picking. Ignored when canvasPos given. * @param {Number[]} [params.direction] World-space ray direction when ray-picking. Also indicates the length of the ray. Ignored when canvasPos given. * @param {Number[]} [params.matrix] 4x4 transformation matrix to define the World-space ray origin and direction, as an alternative to ````origin```` and ````direction````. * @param {Number} [params.snapRadius=30] The snap radius, in canvas pixels. * @param {boolean} [params.snapToVertex=true] Whether to snap to vertex. Only works when `canvasPos` given. * @param {boolean} [params.snapToEdge=true] Whether to snap to edge. Only works when `canvasPos` given. * @param {PickResult} [pickResult] Holds the results of the pick attempt. Will use the Scene's singleton PickResult if you don't supply your own. * @returns {PickResult} Holds results of the pick attempt, returned when an {@link Entity} is picked, else null. See method comments for description. */ pick(params, pickResult) { if (this.canvas.boundary[2] === 0 || this.canvas.boundary[3] === 0) { this.error("Picking not allowed while canvas has zero width or height"); return null; } params = params || {}; params.pickSurface = params.pickSurface || params.rayPick; // Backwards compatibility if (!params.canvasPos && !params.matrix && (!params.origin || !params.direction)) { this.warn("picking without canvasPos, matrix, or ray origin and direction"); } const includeEntities = params.includeEntities || params.include; // Backwards compat if (includeEntities) { params.includeEntityIds = getEntityIDMap(this, includeEntities); } const excludeEntities = params.excludeEntities || params.exclude; // Backwards compat if (excludeEntities) { params.excludeEntityIds = getEntityIDMap(this, excludeEntities); } if (this._needRecompile) { this._recompile(); this._renderer.imageDirty(); this._needRecompile = false; } if (params.snapToEdge || params.snapToVertex) { pickResult = this._renderer.snapPick(params, pickResult); } else { pickResult = this._renderer.pick(params, pickResult); } if (pickResult) { if (pickResult.entity && pickResult.entity.fire) { pickResult.entity.fire("picked", pickResult); // TODO: SceneModelEntity doesn't fire events } } return pickResult; } /** * @param {Object} params Picking parameters. * @param {Number[]} params.canvasPos Canvas-space coordinates. * @param {Number} [params.snapRadius=30] The snap radius, in canvas pixels * @param {boolean} [params.snapToVertex=true] Whether to snap to vertex. * @param {boolean} [params.snapToEdge=true] Whether to snap to edge. * @deprecated */ snapPick(params) { if (undefined === this._warnSnapPickDeprecated) { this._warnSnapPickDeprecated = true; this.warn("Scene.snapPick() is deprecated since v2.4.2 - use Scene.pick() instead"); } if (!params.canvasPos) { this.error("Scene.snapPick() canvasPos parameter expected"); return; } return this._renderer.snapPick(params); } /** * Destroys all non-default {@link Component}s in this Scene. */ clear() { var component; for (const id in this.components) { if (this.components.hasOwnProperty(id)) { component = this.components[id]; if (!component._dontClear) { // Don't destroy components like Camera, Input, Viewport etc. component.destroy(); } } } } /** * Destroys all {@link Light}s in this Scene.. */ clearLights() { const ids = Object.keys(this.lights); for (let i = 0, len = ids.length; i < len; i++) { this.lights[ids[i]].destroy(); } } /** * Destroys all {@link SectionPlane}s in this Scene. */ clearSectionPlanes() { const ids = Object.keys(this.sectionPlanes); for (let i = 0, len = ids.length; i < len; i++) { this.sectionPlanes[ids[i]].destroy(); } } /** * Destroys all {@link Line}s in this Scene. */ clearBitmaps() { const ids = Object.keys(this.bitmaps); for (let i = 0, len = ids.length; i < len; i++) { this.bitmaps[ids[i]].destroy(); } } /** * Destroys all {@link Line}s in this Scene. */ clearLines() { const ids = Object.keys(this.lineSets); for (let i = 0, len = ids.length; i < len; i++) { this.lineSets[ids[i]].destroy(); } } /** * Gets the collective axis-aligned boundary (AABB) of a batch of {@link Entity}s that represent objects. * * An {@link Entity} represents an object when {@link Entity#isObject} is ````true````. * * Each {@link Entity} on which {@link Entity#isObject} is registered by {@link Entity#id} in {@link Scene#visibleObjects}. * * Each {@link Entity} is only included in the AABB when {@link Entity#collidable} is ````true````. * * @param {String[]} ids Array of {@link Entity#id} values. * @returns {[Number, Number, Number, Number, Number, Number]} An axis-aligned World-space bounding box, given as elements ````[xmin, ymin, zmin, xmax, ymax, zmax]````. */ getAABB(ids) { if (ids === undefined) { return this.aabb; } if (utils.isString(ids)) { const entity = this.objects[ids]; if (entity && entity.aabb) { // A Component subclass with an AABB return entity.aabb; } ids = [ids]; // Must be an entity type } if (ids.length === 0) { return this.aabb; } let xmin = math.MAX_DOUBLE; let ymin = math.MAX_DOUBLE; let zmin = math.MAX_DOUBLE; let xmax = math.MIN_DOUBLE; let ymax = math.MIN_DOUBLE; let zmax = math.MIN_DOUBLE; let valid; this.withObjects(ids, entity => { if (entity.collidable) { const aabb = entity.aabb; if (aabb[0] < xmin) { xmin = aabb[0]; } if (aabb[1] < ymin) { ymin = aabb[1]; } if (aabb[2] < zmin) { zmin = aabb[2]; } if (aabb[3] > xmax) { xmax = aabb[3]; } if (aabb[4] > ymax) { ymax = aabb[4]; } if (aabb[5] > zmax) { zmax = aabb[5]; } valid = true; } } ); if (valid) { const aabb2 = math.AABB3(); aabb2[0] = xmin; aabb2[1] = ymin; aabb2[2] = zmin; aabb2[3] = xmax; aabb2[4] = ymax; aabb2[5] = zmax; return aabb2; } else { return this.aabb; // Scene AABB } } /** * Batch-updates {@link Entity#visible} on {@link Entity}s that represent objects. * * An {@link Entity} represents an object when {@link Entity#isObject} is ````true````. * * Each {@link Entity} on which both {@link Entity#isObject} and {@link Entity#visible} are ````true```` is * registered by {@link Entity#id} in {@link Scene#visibleObjects}. * * @param {String[]} ids Array of {@link Entity#id} values. * @param {Boolean} visible Whether or not to set visible. * @returns {Boolean} True if any {@link Entity}s were updated, else false if all updates were redundant and not applied. */ setObjectsVisible(ids, visible) { return this.withObjects(ids, entity => { const changed = (entity.visible !== visible); entity.visible = visible; return changed; }); } /** * Batch-updates {@link Entity#collidable} on {@link Entity}s that represent objects. * * An {@link Entity} represents an object when {@link Entity#isObject} is ````true````. * * @param {String[]} ids Array of {@link Entity#id} values. * @param {Boolean} collidable Whether or not to set collidable. * @returns {Boolean} True if any {@link Entity}s were updated, else false if all updates were redundant and not applied. */ setObjectsCollidable(ids, collidable) { return this.withObjects(ids, entity => { const changed = (entity.collidable !== collidable); entity.collidable = collidable; return changed; }); } /** * Batch-updates {@link Entity#culled} on {@link Entity}s that represent objects. * * An {@link Entity} represents an object when {@link Entity#isObject} is ````true````. * * @param {String[]} ids Array of {@link Entity#id} values. * @param {Boolean} culled Whether or not to cull. * @returns {Boolean} True if any {@link Entity}s were updated, else false if all updates were redundant and not applied. */ setObjectsCulled(ids, culled) { return this.withObjects(ids, entity => { const changed = (entity.culled !== culled); entity.culled = culled; return changed; }); } /** * Batch-updates {@link Entity#selected} on {@link Entity}s that represent objects. * * An {@link Entity} represents an object when {@link Entity#isObject} is ````true````. * * Each {@link Entity} on which both {@link Entity#isObject} and {@link Entity#selected} are ````true```` is * registered by {@link Entity#id} in {@link Scene#selectedObjects}. * * @param {String[]} ids Array of {@link Entity#id} values. * @param {Boolean} selected Whether or not to select. * @returns {Boolean} True if any {@link Entity}s were updated, else false if all updates were redundant and not applied. */ setObjectsSelected(ids, selected) { return this.withObjects(ids, entity => { const changed = (entity.selected !== selected); entity.selected = selected; return changed; }); } /** * Batch-updates {@link Entity#highlighted} on {@link Entity}s that represent objects. * * An {@link Entity} represents an object when {@link Entity#isObject} is ````true````. * * Each {@link Entity} on which both {@link Entity#isObject} and {@link Entity#highlighted} are ````true```` is * registered by {@link Entity#id} in {@link Scene#highlightedObjects}. * * @param {String[]} ids Array of {@link Entity#id} values. * @param {Boolean} highlighted Whether or not to highlight. * @returns {Boolean} True if any {@link Entity}s were updated, else false if all updates were redundant and not applied. */ setObjectsHighlighted(ids, highlighted) { return this.withObjects(ids, entity => { const changed = (entity.highlighted !== highlighted); entity.highlighted = highlighted; return changed; }); } /** * Batch-updates {@link Entity#xrayed} on {@link Entity}s that represent objects. * * An {@link Entity} represents an object when {@link Entity#isObject} is ````true````. * * @param {String[]} ids Array of {@link Entity#id} values. * @param {Boolean} xrayed Whether or not to xray. * @returns {Boolean} True if any {@link Entity}s were updated, else false if all updates were redundant and not applied. */ setObjectsXRayed(ids, xrayed) { return this.withObjects(ids, entity => { const changed = (entity.xrayed !== xrayed); entity.xrayed = xrayed; return changed; }); } /** * Batch-updates {@link Entity#edges} on {@link Entity}s that represent objects. * * An {@link Entity} represents an object when {@link Entity#isObject} is ````true````. * * @param {String[]} ids Array of {@link Entity#id} values. * @param {Boolean} edges Whether or not to show edges. * @returns {Boolean} True if any {@link Entity}s were updated, else false if all updates were redundant and not applied. */ setObjectsEdges(ids, edges) { return this.withObjects(ids, entity => { const changed = (entity.edges !== edges); entity.edges = edges; return changed; }); } /** * Batch-updates {@link Entity#colorize} on {@link Entity}s that represent objects. * * An {@link Entity} represents an object when {@link Entity#isObject} is ````true````. * * @param {String[]} ids Array of {@link Entity#id} values. * @param {Number[]} [colorize=(1,1,1)] RGB colorize factors, multiplied by the rendered pixel colors. * @returns {Boolean} True if any {@link Entity}s changed opacity, else false if all updates were redundant and not applied. */ setObjectsColorized(ids, colorize) { return this.withObjects(ids, entity => { entity.colorize = colorize; }); } /** * Batch-updates {@link Entity#opacity} on {@link Entity}s that represent objects. * * An {@link Entity} represents an object when {@link Entity#isObject} is ````true````. * * @param {String[]} ids Array of {@link Entity#id} values. * @param {Number} [opacity=1.0] Opacity factor, multiplied by the rendered pixel alphas. * @returns {Boolean} True if any {@link Entity}s changed opacity, else false if all updates were redundant and not applied. */ setObjectsOpacity(ids, opacity) { return this.withObjects(ids, entity => { const changed = (entity.opacity !== opacity); entity.opacity = opacity; return changed; }); } /** * Batch-updates {@link Entity#pickable} on {@link Entity}s that represent objects. * * An {@link Entity} represents an object when {@link Entity#isObject} is ````true````. * * @param {String[]} ids Array of {@link Entity#id} values. * @param {Boolean} pickable Whether or not to set pickable. * @returns {Boolean} True if any {@link Entity}s were updated, else false if all updates were redundant and not applied. */ setObjectsPickable(ids, pickable) { return this.withObjects(ids, entity => { const changed = (entity.pickable !== pickable); entity.pickable = pickable; return changed; }); } /** * Batch-updates {@link Entity#offset} on {@link Entity}s that represent objects. * * An {@link Entity} represents an object when {@link Entity#isObject} is ````true````. * * @param {String[]} ids Array of {@link Entity#id} values. * @param {Number[]} [offset] 3D offset vector. */ setObjectsOffset(ids, offset) { this.withObjects(ids, entity => { entity.offset = offset; }); } /** * Iterates with a callback over {@link Entity}s that represent objects. * * An {@link Entity} represents an object when {@link Entity#isObject} is ````true````. * * @param {String[]} ids Array of {@link Entity#id} values. * @param {Function} callback Callback to execute on eacn {@link Entity}. * @returns {Boolean} True if any {@link Entity}s were updated, else false if all updates were redundant and not applied. */ withObjects(ids, callback) { if (utils.isString(ids)) { ids = [ids]; } let changed = false; for (let i = 0, len = ids.length; i < len; i++) { const id = ids[i]; let entity = this.objects[id]; if (entity) { changed = callback(entity) || changed; } else { const modelIds = this.modelIds; for (let i = 0, len = modelIds.length; i < len; i++) { const modelId = modelIds[i]; const globalObjectId = math.globalizeObjectId(modelId, id); entity = this.objects[globalObjectId]; if (entity) { changed = callback(entity) || changed; } } } } return changed; } /** * This method will "tickify" the provided `cb` function. * * This means, the function will be wrapped so: * * - it runs time-aligned to scene ticks * - it runs maximum once per scene-tick * * @param {Function} cb The function to tickify * @returns {Function} */ tickify(cb) { const cbString = cb.toString(); /** * Check if the function is already tickified, and if so return the cached one. */ if (cbString in this._tickifiedFunctions) { return this._tickifiedFunctions[cbString].wrapperFunc; } let alreadyRun = 0; let needToRun = 0; let lastArgs; /** * The provided `cb` function is replaced with a "set-dirty" function * * @type {Function} */ const wrapperFunc = function (...args) { lastArgs = args; needToRun++; }; /** * An each scene tick, if the "dirty-flag" is set, run the `cb` function. * * This will make it run time-aligned to the scene tick. */ const tickSubId = this.on("tick", () => { const tmp = needToRun; if (tmp > alreadyRun) { alreadyRun = tmp; cb(...lastArgs); } }); /** * And, store the list of subscribers. */ this._tickifiedFunctions[cbString] = {tickSubId, wrapperFunc}; return wrapperFunc; } /** * Destroys this Scene. */ destroy() { super.destroy(); for (const id in this.components) { if (this.components.hasOwnProperty(id)) { this.components[id].destroy(); } } //destroy section caps separately because it's not a component this._sectionCaps.destroy(); this.canvas.gl = null; // Memory leak prevention this.components = null; this.models = null; this.objects = null; this.visibleObjects = null; this.xrayedObjects = null; this.highlightedObjects = null; this.selectedObjects = null; this.colorizedObjects = null; this.opacityObjects = null; this.sectionPlanes = null; this.lights = null; this.lightMaps = null; this.reflectionMaps = null; this._objectIds = null; this._visibleObjectIds = null; this._xrayedObjectIds = null; this._highlightedObjectIds = null; this._selectedObjectIds = null; this._colorizedObjectIds = null; this._sectionCaps = null; this.types = null; this.components = null; this.canvas = null; this._renderer = null; this.input = null; this._viewport = null; this._camera = null; } } const screenPos = math.vec4(); const viewPos = math.vec4(); const tempVec3a$9 = math.vec3(); const tempVec3b$6 = math.vec3(); const tempVec3c$5 = math.vec3(); const tempVec4a$6 = math.vec4(); const tempVec4b$6 = math.vec4(); const tempVec4c$1 = math.vec4(); /** * @private */ class PanController { constructor(scene) { this._scene = scene; } /** * Dollys the Camera towards the given target 2D canvas position. * * When the target's corresponding World-space position is also provided, then this function will also test if we've * dollied past the target, and will return ````true```` if that's the case. * * @param [optionalTargetWorldPos] Optional world position of the target * @param targetCanvasPos Canvas position of the target * @param dollyDelta Amount to dolly * @return True if optionalTargetWorldPos was given, and we've dollied past that position. */ dollyToCanvasPos(optionalTargetWorldPos, targetCanvasPos, dollyDelta) { let dolliedThroughSurface = false; const camera = this._scene.camera; if (optionalTargetWorldPos) { const eyeToWorldPosVec = math.subVec3(optionalTargetWorldPos, camera.eye, tempVec3a$9); const eyeWorldPosDist = math.lenVec3(eyeToWorldPosVec); dolliedThroughSurface = (eyeWorldPosDist < dollyDelta); } if (camera.projection === "perspective") { const unprojectedWorldPos = this._unproject(targetCanvasPos, tempVec4a$6); const offset = math.subVec3(unprojectedWorldPos, camera.eye, tempVec4c$1); const moveVec = math.mulVec3Scalar(math.normalizeVec3(offset), -dollyDelta, []); camera.eye = [camera.eye[0] - moveVec[0], camera.eye[1] - moveVec[1], camera.eye[2] - moveVec[2]]; camera.look = [camera.look[0] - moveVec[0], camera.look[1] - moveVec[1], camera.look[2] - moveVec[2]]; if (optionalTargetWorldPos) { // Subtle UX tweak - if we have a target World position, then set camera eye->look distance to // the same distance as from eye->target. This just gives us a better position for look, // if we subsequently orbit eye about look, so that we don't orbit a position that's // suddenly a lot closer than the point we pivoted about on the surface of the last object // that we click-drag-pivoted on. const eyeTargetVec = math.subVec3(optionalTargetWorldPos, camera.eye, tempVec3a$9); const lenEyeTargetVec = math.lenVec3(eyeTargetVec); const eyeLookVec = math.mulVec3Scalar(math.normalizeVec3(math.subVec3(camera.look, camera.eye, tempVec3b$6)), lenEyeTargetVec); camera.look = [camera.eye[0] + eyeLookVec[0], camera.eye[1] + eyeLookVec[1], camera.eye[2] + eyeLookVec[2]]; } } else if (camera.projection === "ortho") { // - set ortho scale, getting the unprojected targetCanvasPos before and after, get that difference in a vector; // - get the vector in which we're dollying; // - add both vectors to camera eye and look. const worldPos1 = this._unproject(targetCanvasPos, tempVec4a$6); camera.ortho.scale = camera.ortho.scale - dollyDelta; camera.ortho._update(); // HACK const worldPos2 = this._unproject(targetCanvasPos, tempVec4b$6); const offset = math.subVec3(worldPos2, worldPos1, tempVec4c$1); const moveVec = offset; camera.eye = [camera.eye[0] - moveVec[0], camera.eye[1] - moveVec[1], camera.eye[2] - moveVec[2]]; camera.look = [camera.look[0] - moveVec[0], camera.look[1] - moveVec[1], camera.look[2] - moveVec[2]]; } return dolliedThroughSurface; } _unproject(canvasPos, worldPos) { const camera = this._scene.camera; const transposedProjectMat = camera.project.transposedMatrix; const Pt3 = transposedProjectMat.subarray(8, 12); const Pt4 = transposedProjectMat.subarray(12); const D = [0, 0, -1.0, 1]; const screenZ = math.dotVec4(D, Pt3) / math.dotVec4(D, Pt4); camera.project.unproject(canvasPos, screenZ, screenPos, viewPos, worldPos); return worldPos; } destroy() { } } const tempVec3a$8 = math.vec3(); const tempVec3b$5 = math.vec3(); const tempVec3c$4 = math.vec3(); const tempVec4a$5 = math.vec4(); const tempVec4b$5 = math.vec4(); const tempVec4c = math.vec4(); const TOP_LIMIT = 0.001; const BOTTOM_LIMIT = Math.PI - 0.001; /** @private */ class PivotController { /** * @private */ constructor(scene, configs) { // Pivot math by: http://www.derschmale.com/ this._scene = scene; this._configs = configs; this._pivotWorldPos = math.vec3(); this._cameraOffset = math.vec3(); this._azimuth = 0; this._polar = 0; this._radius = 0; this._pivotPosSet = false; // Initially false, true as soon as _pivotWorldPos has been set to some value this._pivoting = false; // True while pivoting this._shown = false; this._pivotSphereEnabled = false; this._pivotSphere = null; this._pivotSphereSize = 1; this._pivotSphereGeometry = null; this._pivotSphereMaterial = null; this._rtcCenter = math.vec3(); this._rtcPos = math.vec3(); this._pivotViewPos = math.vec4(); this._pivotProjPos = math.vec4(); this._pivotCanvasPos = math.vec2(); this._cameraDirty = true; this._onViewMatrix = this._scene.camera.on("viewMatrix", () => { this._cameraDirty = true; }); this._onProjMatrix = this._scene.camera.on("projMatrix", () => { this._cameraDirty = true; }); this._onTick = this._scene.on("tick", () => { this.updatePivotElement(); this.updatePivotSphere(); }); } createPivotSphere() { const currentPos = this.getPivotPos(); const cameraPos = math.vec3(); math.decomposeMat4(math.inverseMat4(this._scene.viewer.camera.viewMatrix, math.mat4()), cameraPos, math.vec4(), math.vec3()); const length = math.distVec3(cameraPos, currentPos); let radius = (Math.tan(Math.PI / 500) * length) * this._pivotSphereSize; if (this._scene.camera.projection == "ortho") { radius /= (this._scene.camera.ortho.scale / 2); } worldToRTCPos(currentPos, this._rtcCenter, this._rtcPos); this._pivotSphereGeometry = new VBOGeometry( this._scene, buildSphereGeometry({ radius }) ); this._pivotSphere = new Mesh(this._scene, { geometry: this._pivotSphereGeometry, material: this._pivotSphereMaterial, pickable: false, position: this._rtcPos, rtcCenter: this._rtcCenter }); }; destroyPivotSphere() { if (this._pivotSphere) { this._pivotSphere.destroy(); this._pivotSphere = null; } if (this._pivotSphereGeometry) { this._pivotSphereGeometry.destroy(); this._pivotSphereGeometry = null; } } updatePivotElement() { const camera = this._scene.camera; const canvas = this._scene.canvas; if (this._pivoting && this._cameraDirty) { math.transformPoint3(camera.viewMatrix, this.getPivotPos(), this._pivotViewPos); this._pivotViewPos[3] = 1; math.transformPoint4(camera.projMatrix, this._pivotViewPos, this._pivotProjPos); const canvasAABB = canvas.boundary; const canvasWidth = canvasAABB[2]; const canvasHeight = canvasAABB[3]; this._pivotCanvasPos[0] = Math.floor((1 + this._pivotProjPos[0] / this._pivotProjPos[3]) * canvasWidth / 2); this._pivotCanvasPos[1] = Math.floor((1 - this._pivotProjPos[1] / this._pivotProjPos[3]) * canvasHeight / 2); // data-textures: avoid to do continuous DOM layout calculations let canvasBoundingRect = canvas._lastBoundingClientRect; if (!canvasBoundingRect || canvas._canvasSizeChanged) { const canvasElem = canvas.canvas; canvasBoundingRect = canvas._lastBoundingClientRect = canvasElem.getBoundingClientRect (); } if (this._pivotElement) { this._pivotElement.style.left = (Math.floor(canvasBoundingRect.left + this._pivotCanvasPos[0]) - (this._pivotElement.clientWidth / 2) + window.scrollX) + "px"; this._pivotElement.style.top = (Math.floor(canvasBoundingRect.top + this._pivotCanvasPos[1]) - (this._pivotElement.clientHeight / 2) + window.scrollY) + "px"; } this._cameraDirty = false; } } updatePivotSphere() { if (this._pivoting && this._pivotSphere) { worldToRTCPos(this.getPivotPos(), this._rtcCenter, this._rtcPos); if(!math.compareVec3(this._rtcPos, this._pivotSphere.position)) { this.destroyPivotSphere(); this.createPivotSphere(); } } } /** * Sets the HTML DOM element that will represent the pivot position. * * @param pivotElement */ setPivotElement(pivotElement) { this._pivotElement = pivotElement; } /** * Sets a sphere as the representation of the pivot position. * * @param {Object} [cfg] Sphere configuration. * @param {String} [cfg.size=1] Optional size factor of the sphere. Defaults to 1. * @param {String} [cfg.color=Array] Optional maretial color. Defaults to a red. */ enablePivotSphere(cfg = {}) { this.destroyPivotSphere(); this._pivotSphereEnabled = true; if (cfg.size) { this._pivotSphereSize = cfg.size; } const color = cfg.color || [1, 0, 0]; this._pivotSphereMaterial = new PhongMaterial(this._scene, { emissive: color, ambient: color, specular: [0,0,0], diffuse: [0,0,0], }); } /** * Remove the sphere as the representation of the pivot position. * */ disablePivotSphere() { this.destroyPivotSphere(); this._pivotSphereEnabled = false; } /** * Begins pivoting. */ startPivot() { const camera = this._scene.camera; let lookat = math.lookAtMat4v(camera.eye, camera.look, camera.up); math.transformPoint3(lookat, this.getPivotPos(), this._cameraOffset); const pivotPos = this.getPivotPos(); this._cameraOffset[2] += math.distVec3(camera.eye, pivotPos); lookat = math.inverseMat4(lookat); const offset = math.transformVec3(lookat, this._cameraOffset); const diff = math.vec3(); math.subVec3(camera.eye, pivotPos, diff); math.addVec3(diff, offset); if (camera.zUp) { const t = diff[1]; diff[1] = diff[2]; diff[2] = t; } this._radius = math.lenVec3(diff); this._polar = Math.acos(diff[1] / this._radius); this._azimuth = Math.atan2(diff[0], diff[2]); this._pivoting = true; } _cameraLookingDownwards() { // Returns true if angle between camera viewing direction and World-space "up" axis is too small const camera = this._scene.camera; const forwardAxis = math.normalizeVec3(math.subVec3(camera.look, camera.eye, tempVec3a$8)); const rightAxis = math.cross3Vec3(forwardAxis, camera.worldUp, tempVec3b$5); let rightAxisLen = math.sqLenVec3(rightAxis); return (rightAxisLen <= 0.0001); } /** * Returns true if we are currently pivoting. * * @returns {Boolean} */ getPivoting() { return this._pivoting; } /** * Sets a 3D World-space position to pivot about. * * @param {Number[]} worldPos The new World-space pivot position. */ setPivotPos(worldPos) { this._pivotWorldPos.set(worldPos); this._pivotPosSet = true; } /** * Sets the pivot position to the 3D projection of the given 2D canvas coordinates on a sphere centered * at the viewpoint. The radius of the sphere is configured via {@link CameraControl#smartPivot}. * * * @param canvasPos */ setCanvasPivotPos(canvasPos) { const camera = this._scene.camera; const pivotShereRadius = Math.abs(math.distVec3(this._scene.center, camera.eye)); const transposedProjectMat = camera.project.transposedMatrix; const Pt3 = transposedProjectMat.subarray(8, 12); const Pt4 = transposedProjectMat.subarray(12); const D = [0, 0, -1.0, 1]; const screenZ = math.dotVec4(D, Pt3) / math.dotVec4(D, Pt4); const worldPos = tempVec4a$5; camera.project.unproject(canvasPos, screenZ, tempVec4b$5, tempVec4c, worldPos); const eyeWorldPosVec = math.normalizeVec3(math.subVec3(worldPos, camera.eye, tempVec3a$8)); const posOnSphere = math.addVec3(camera.eye, math.mulVec3Scalar(eyeWorldPosVec, pivotShereRadius, tempVec3b$5), tempVec3c$4); this.setPivotPos(posOnSphere); } /** * Gets the current position we're pivoting about. * @returns {Number[]} The current World-space pivot position. */ getPivotPos() { return (this._pivotPosSet) ? this._pivotWorldPos : this._scene.camera.look; // Avoid pivoting about [0,0,0] by default } /** * Continues to pivot. * * @param {Number} yawInc Yaw rotation increment. * @param {Number} pitchInc Pitch rotation increment. */ continuePivot(yawInc, pitchInc) { if (!this._pivoting) { return; } if (yawInc === 0 && pitchInc === 0) { return; } const camera = this._scene.camera; var dx = -yawInc; const dy = -pitchInc; if (camera.worldUp[2] === 1) { dx = -dx; } this._azimuth += -dx * .01; const isMovingUp = dy < 0; const isMovingDown = dy > 0; // Track if we're at limits - only check if we're very close to the limit const atTopLimit = Math.abs(this._polar - TOP_LIMIT) < 0.005; const atBottomLimit = Math.abs(this._polar - BOTTOM_LIMIT) < 0.005; let newPolar = this._polar + dy * .01; // Case 1: At top limit and trying to go beyond if (atTopLimit && isMovingUp) { newPolar = TOP_LIMIT; } // Case 2: At bottom limit and trying to go beyond else if (atBottomLimit && isMovingDown) { newPolar = BOTTOM_LIMIT; } // Case 3: Normal rotation or moving away from a limit else { newPolar = math.clamp(newPolar, TOP_LIMIT, BOTTOM_LIMIT); } this._polar = newPolar; const pos = [ this._radius * Math.sin(this._polar) * Math.sin(this._azimuth), this._radius * Math.cos(this._polar), this._radius * Math.sin(this._polar) * Math.cos(this._azimuth) ]; if (camera.worldUp[2] === 1) { const t = pos[1]; pos[1] = pos[2]; pos[2] = t; } // Preserve the eye->look distance, since in xeokit "look" is the point-of-interest, not the direction vector. const eyeLookLen = math.lenVec3(math.subVec3(camera.look, camera.eye, math.vec3())); const pivotPos = this.getPivotPos(); math.addVec3(pos, pivotPos); let lookat = math.lookAtMat4v(pos, pivotPos, camera.worldUp); lookat = math.inverseMat4(lookat); const offset = math.transformVec3(lookat, this._cameraOffset); lookat[12] -= offset[0]; lookat[13] -= offset[1]; lookat[14] -= offset[2]; const zAxis = [lookat[8], lookat[9], lookat[10]]; camera.eye = [lookat[12], lookat[13], lookat[14]]; math.subVec3(camera.eye, math.mulVec3Scalar(zAxis, eyeLookLen), camera.look); camera.up = [lookat[4], lookat[5], lookat[6]]; this.showPivot(); } /** * Shows the pivot position. * * Only works if we set an HTML DOM element to represent the pivot position. */ showPivot() { if (this._shown) { return; } if (this._pivotElement) { this.updatePivotElement(); this._pivotElement.style.visibility = "visible"; } if (this._pivotSphereEnabled) { this.destroyPivotSphere(); this.createPivotSphere(); } this._shown = true; } /** * Hides the pivot position. * * Only works if we set an HTML DOM element to represent the pivot position. */ hidePivot() { if (!this._shown) { return; } if (this._pivotElement) { this._pivotElement.style.visibility = "hidden"; } if (this._pivotSphereEnabled) { this.destroyPivotSphere(); } this._shown = false; } /** * Finishes pivoting. */ endPivot() { this._pivoting = false; } destroy() { this.destroyPivotSphere(); this._scene.camera.off(this._onViewMatrix); this._scene.camera.off(this._onProjMatrix); this._scene.off(this._onTick); } } /** * * @private */ class PickController { constructor(cameraControl, configs) { /** * @type {Scene} */ this._scene = cameraControl.scene; this._cameraControl = cameraControl; this._scene.canvas.canvas.oncontextmenu = function (e) { e.preventDefault(); }; this._configs = configs; /** * Set true to schedule picking of an Entity. * @type {boolean} */ this.schedulePickEntity = false; /** * Set true to schedule picking of a position on teh surface of an Entity. * @type {boolean} */ this.schedulePickSurface = false; /** * Set true to schedule snap-picking with surface picking as a fallback - used for measurement. * @type {boolean} */ this.scheduleSnapOrPick = false; /** * The canvas position at which to do the next scheduled pick. * @type {Number[]} */ this.pickCursorPos = math.vec2(); /** * Will be true after picking to indicate that something was picked. * @type {boolean} */ this.picked = false; /** * Will be true after picking to indicate that a position on the surface of an Entity was picked. * @type {boolean} */ this.pickedSurface = false; /** * Will hold the PickResult after after picking. * @type {PickResult} */ this.pickResult = null; this._lastPickedEntityId = null; this._lastHash = null; this._needFireEvents = 0; } /** * Immediately attempts a pick, if scheduled. */ update() { if (!this._configs.pointerEnabled) { return; } if (!this.schedulePickEntity && !this.schedulePickSurface) { return; } const hash = `${~~this.pickCursorPos[0]}-${~~this.pickCursorPos[1]}-${this.scheduleSnapOrPick}-${this.schedulePickSurface}-${this.schedulePickEntity}`; if (this._lastHash === hash) { return; } this.picked = false; this.pickedSurface = false; this.snappedOrPicked = false; this.hoveredSnappedOrSurfaceOff = false; const hasHoverSurfaceSubs = this._cameraControl.hasSubs("hoverSurface"); if (this.scheduleSnapOrPick) { const snapPickResult = this._scene.pick({ canvasPos: this.pickCursorPos, snapRadius: this._configs.snapRadius, snapToVertex: this._configs.snapToVertex, snapToEdge: this._configs.snapToEdge, }); if (snapPickResult && (snapPickResult.snappedToEdge || snapPickResult.snappedToVertex)) { this.snapPickResult = snapPickResult; this.snappedOrPicked = true; this._needFireEvents++; } else { this.schedulePickSurface = true; // Fallback this.snapPickResult = null; } } if (this.schedulePickSurface) { if (this.pickResult && this.pickResult.worldPos) { const pickResultCanvasPos = this.pickResult.canvasPos; if (pickResultCanvasPos[0] === this.pickCursorPos[0] && pickResultCanvasPos[1] === this.pickCursorPos[1]) { this.picked = true; this.pickedSurface = true; this._needFireEvents += hasHoverSurfaceSubs ? 1 : 0; this.schedulePickEntity = false; this.schedulePickSurface = false; if (this.scheduleSnapOrPick) { this.snappedOrPicked = true; } else { this.hoveredSnappedOrSurfaceOff = true; } this.scheduleSnapOrPick = false; return; } } } if (this.schedulePickEntity) { if (this.pickResult && (this.pickResult.canvasPos || this.pickResult.snappedCanvasPos)) { const pickResultCanvasPos = this.pickResult.canvasPos || this.pickResult.snappedCanvasPos; if (pickResultCanvasPos[0] === this.pickCursorPos[0] && pickResultCanvasPos[1] === this.pickCursorPos[1]) { this.picked = true; this.pickedSurface = false; this.schedulePickEntity = false; this.schedulePickSurface = false; return; } } } if (this.schedulePickSurface || (this.scheduleSnapOrPick && !this.snapPickResult)) { this.pickResult = this._scene.pick({ pickSurface: true, pickSurfaceNormal: false, canvasPos: this.pickCursorPos }); if (this.pickResult) { this.picked = true; if (this.scheduleSnapOrPick) { this.snappedOrPicked = true; } else { this.pickedSurface = true; } this._needFireEvents++; } else if (this.scheduleSnapOrPick) { this.hoveredSnappedOrSurfaceOff = true; this._needFireEvents++; } } else { // schedulePickEntity == true this.pickResult = this._scene.pick({ canvasPos: this.pickCursorPos }); if (this.pickResult) { this.picked = true; this.pickedSurface = false; this._needFireEvents++; } } this.scheduleSnapOrPick = false; this.schedulePickEntity = false; this.schedulePickSurface = false; } fireEvents() { if (this._needFireEvents === 0) { return; } if (this.hoveredSnappedOrSurfaceOff) { this._cameraControl.fire("hoverSnapOrSurfaceOff", { canvasPos: this.pickCursorPos, pointerPos : this.pickCursorPos }, true); } if (this.snappedOrPicked) { if (this.snapPickResult) { const pickResult = new PickResult(); pickResult.entity = this.snapPickResult.entity; pickResult.snappedToVertex = this.snapPickResult.snappedToVertex; pickResult.snappedToEdge = this.snapPickResult.snappedToEdge; pickResult.worldPos = this.snapPickResult.worldPos; pickResult.canvasPos = this.pickCursorPos; pickResult.snappedCanvasPos = this.snapPickResult.snappedCanvasPos; this._cameraControl.fire("hoverSnapOrSurface", pickResult, true); this.snapPickResult = null; } else { this._cameraControl.fire("hoverSnapOrSurface", this.pickResult, true); } } if (this.picked && this.pickResult && (this.pickResult.entity || this.pickResult.worldPos)) { if (this.pickResult.entity) { const pickedEntityId = this.pickResult.entity.id; if (this._lastPickedEntityId !== pickedEntityId) { if (this._lastPickedEntityId !== undefined) { this._cameraControl.fire("hoverOut", { entity: this._scene.objects[this._lastPickedEntityId] }, true); } this._cameraControl.fire("hoverEnter", this.pickResult, true); this._lastPickedEntityId = pickedEntityId; } } this._cameraControl.fire("hover", this.pickResult, true); if (this.pickResult.worldPos) { this.pickedSurface = true; this._cameraControl.fire("hoverSurface", this.pickResult, true); } } else { if (this._lastPickedEntityId !== undefined) { this._cameraControl.fire("hoverOut", { entity: this._scene.objects[this._lastPickedEntityId] }, true); this._lastPickedEntityId = undefined; } this._cameraControl.fire("hoverOff", { canvasPos: this.pickCursorPos }, true); } this.pickResult = null; this._needFireEvents = 0; } } /** * @private */ const canvasPos = math.vec2(); const getCanvasPosFromEvent$3 = function (event, canvasPos) { if (!event) { event = window.event; canvasPos[0] = event.x; canvasPos[1] = event.y; } else { let element = event.target; let totalOffsetLeft = 0; let totalOffsetTop = 0; let totalScrollX = 0; let totalScrollY = 0; while (element.offsetParent) { totalOffsetLeft += element.offsetLeft; totalOffsetTop += element.offsetTop; totalScrollX += element.scrollLeft; totalScrollY += element.scrollTop; element = element.offsetParent; } canvasPos[0] = event.pageX + totalScrollX - totalOffsetLeft; canvasPos[1] = event.pageY + totalScrollY - totalOffsetTop; } return canvasPos; }; /** * @private */ class MousePanRotateDollyHandler { constructor(scene, controllers, configs, states, updates) { this._scene = scene; const pickController = controllers.pickController; const cameraControl = controllers.cameraControl; let lastX = 0; let lastY = 0; let lastXDown = 0; let lastYDown = 0; let mouseDownLeft; let mouseDownMiddle; let mouseDownRight; let mouseDownPicked = false; const pickedWorldPos = math.vec3(); let mouseMovedOnCanvasSinceLastWheel = true; const canvas = this._scene.canvas.canvas; const keyDown = []; document.addEventListener("keydown", this._documentKeyDownHandler = (e) => { if (!(configs.active && configs.pointerEnabled) || (!scene.input.keyboardEnabled)) { return; } const keyCode = e.keyCode; keyDown[keyCode] = true; }); document.addEventListener("keyup", this._documentKeyUpHandler = (e) => { if (!(configs.active && configs.pointerEnabled) || (!scene.input.keyboardEnabled)) { return; } const keyCode = e.keyCode; keyDown[keyCode] = false; }); document.addEventListener("visibilitychange", this._onVisibilityChange = () => { keyDown.splice(0); }); window.addEventListener("blur", this._onBlur = () => { keyDown.splice(0); }); function setMousedownState(pick = true) { setMousedownPositions(); if (pick) { setMousedownPick(); } } function setMousedownPositions() { lastX = states.pointerCanvasPos[0]; lastY = states.pointerCanvasPos[1]; lastXDown = states.pointerCanvasPos[0]; lastYDown = states.pointerCanvasPos[1]; } function setMousedownPick() { pickController.pickCursorPos = states.pointerCanvasPos; pickController.schedulePickSurface = true; pickController.update(); if (pickController.picked && pickController.pickedSurface && pickController.pickResult && pickController.pickResult.worldPos) { mouseDownPicked = true; pickedWorldPos.set(pickController.pickResult.worldPos); } else { mouseDownPicked = false; } } function isPanning() { return configs.planView || cameraControl._isKeyDownForAction(cameraControl.MOUSE_PAN, keyDown); } function isRotating() { return cameraControl._isKeyDownForAction(cameraControl.MOUSE_ROTATE, keyDown); } canvas.addEventListener("mousedown", this._mouseDownHandler = (e) => { if (!(configs.active && configs.pointerEnabled)) { return; } switch (e.which) { case 1: // Left button if (keyDown[scene.input.KEY_SHIFT] || configs.planView) { mouseDownLeft = true; keyDown[scene.input.MOUSE_LEFT_BUTTON] = true; setMousedownState(); } else { mouseDownLeft = true; keyDown[scene.input.MOUSE_LEFT_BUTTON] = true; setMousedownState(false); } break; case 2: // Middle/both buttons mouseDownMiddle = true; keyDown[scene.input.MOUSE_MIDDLE_BUTTON] = true; setMousedownState(); break; case 3: // Right button mouseDownRight = true; keyDown[scene.input.MOUSE_RIGHT_BUTTON] = true; if (configs.panRightClick) { setMousedownState(); } break; } }); document.addEventListener("mousemove", this._documentMouseMoveHandler = (e) => { if (!(configs.active && configs.pointerEnabled)) { return; } if (!mouseDownLeft && !mouseDownMiddle && !mouseDownRight) { return; } // Scaling drag-rotate to canvas boundary const canvasBoundary = scene.canvas.boundary; const canvasWidth = canvasBoundary[2]; const canvasHeight = canvasBoundary[3]; const x = states.pointerCanvasPos[0]; const y = states.pointerCanvasPos[1]; const panning = isPanning(); const rotating = isRotating(); const xDelta = document.pointerLockElement ? e.movementX : (x - lastX); const yDelta = document.pointerLockElement ? e.movementY : (y - lastY); if (panning) { const camera = scene.camera; // We use only canvasHeight here so that aspect ratio does not distort speed if (camera.projection === "perspective") { const depth = Math.abs(mouseDownPicked ? math.lenVec3(math.subVec3(pickedWorldPos, scene.camera.eye, [])) : scene.camera.eyeLookDist); const targetDistance = depth * Math.tan((camera.perspective.fov / 2) * Math.PI / 180.0); updates.panDeltaX += (1.5 * xDelta * targetDistance / canvasHeight); updates.panDeltaY += (1.5 * yDelta * targetDistance / canvasHeight); } else { updates.panDeltaX += 0.5 * camera.ortho.scale * (xDelta / canvasHeight); updates.panDeltaY += 0.5 * camera.ortho.scale * (yDelta / canvasHeight); } } else if (rotating) { if (!configs.planView) { // No rotating in plan-view mode if (configs.firstPerson) { updates.rotateDeltaY -= (xDelta / canvasWidth) * configs.dragRotationRate / 2; updates.rotateDeltaX += (yDelta / canvasHeight) * (configs.dragRotationRate / 4); } else { updates.rotateDeltaY -= (xDelta / canvasWidth) * (configs.dragRotationRate * 1.5); updates.rotateDeltaX += (yDelta / canvasHeight) * (configs.dragRotationRate * 1.5); } } } lastX = x; lastY = y; }); canvas.addEventListener("mousemove", this._canvasMouseMoveHandler = (e) => { if (!(configs.active && configs.pointerEnabled)) { return; } if (!states.mouseover) { return; } mouseMovedOnCanvasSinceLastWheel = true; }); document.addEventListener("mouseup", this._documentMouseUpHandler = (e) => { if (!(configs.active && configs.pointerEnabled)) { return; } switch (e.which) { case 1: // Left button mouseDownLeft = false; mouseDownMiddle = false; mouseDownRight = false; keyDown[scene.input.MOUSE_LEFT_BUTTON] = false; keyDown[scene.input.MOUSE_MIDDLE_BUTTON] = false; keyDown[scene.input.MOUSE_RIGHT_BUTTON] = false; break; case 2: // Middle/both buttons mouseDownLeft = false; mouseDownMiddle = false; mouseDownRight = false; keyDown[scene.input.MOUSE_LEFT_BUTTON] = false; keyDown[scene.input.MOUSE_MIDDLE_BUTTON] = false; keyDown[scene.input.MOUSE_RIGHT_BUTTON] = false; break; case 3: // Right button mouseDownLeft = false; mouseDownMiddle = false; mouseDownRight = false; keyDown[scene.input.MOUSE_LEFT_BUTTON] = false; keyDown[scene.input.MOUSE_MIDDLE_BUTTON] = false; keyDown[scene.input.MOUSE_RIGHT_BUTTON] = false; break; } }); canvas.addEventListener("mouseup", this._mouseUpHandler = (e) => { if (!(configs.active && configs.pointerEnabled)) { return; } switch (e.which) { case 3: // Right button getCanvasPosFromEvent$3(e, canvasPos); const x = canvasPos[0]; const y = canvasPos[1]; if (Math.abs(x - lastXDown) < 3 && Math.abs(y - lastYDown) < 3) { controllers.cameraControl.fire("rightClick", { // For context menus pagePos: [Math.round(e.pageX), Math.round(e.pageY)], canvasPos: canvasPos, event: e }, true); } break; } canvas.style.removeProperty("cursor"); }); canvas.addEventListener("mouseenter", this._mouseEnterHandler = () => { if (!(configs.active && configs.pointerEnabled)) { return; } }); const maxElapsed = 1 / 20; const minElapsed = 1 / 60; let secsNowLast = null; canvas.addEventListener("wheel", this._mouseWheelHandler = (e) => { if (!(configs.active && configs.pointerEnabled && configs.zoomOnMouseWheel && cameraControl._isKeyDownForAction(cameraControl.MOUSE_DOLLY, keyDown))) { return; } const secsNow = performance.now() / 1000.0; var secsElapsed = (secsNowLast !== null) ? (secsNow - secsNowLast) : 0; secsNowLast = secsNow; if (secsElapsed > maxElapsed) { secsElapsed = maxElapsed; } if (secsElapsed < minElapsed) { secsElapsed = minElapsed; } const delta = Math.max(-1, Math.min(1, -e.deltaY * 40)); if (delta === 0) { return; } const normalizedDelta = delta / Math.abs(delta); updates.dollyDelta += -normalizedDelta * secsElapsed * configs.mouseWheelDollyRate; if (mouseMovedOnCanvasSinceLastWheel) { states.followPointerDirty = true; mouseMovedOnCanvasSinceLastWheel = false; } }, {passive: true}); } reset() { } destroy() { const canvas = this._scene.canvas.canvas; document.removeEventListener("keydown", this._documentKeyDownHandler); document.removeEventListener("keyup", this._documentKeyUpHandler); document.removeEventListener("visibilitychange", this._onVisibilityChange); window.removeEventListener("blur", this._onBlur); canvas.removeEventListener("mousedown", this._mouseDownHandler); document.removeEventListener("mousemove", this._documentMouseMoveHandler); canvas.removeEventListener("mousemove", this._canvasMouseMoveHandler); document.removeEventListener("mouseup", this._documentMouseUpHandler); canvas.removeEventListener("mouseup", this._mouseUpHandler); canvas.removeEventListener("mouseenter", this._mouseEnterHandler); canvas.removeEventListener("wheel", this._mouseWheelHandler); } } const center = math.vec3(); const tempVec3a$7 = math.vec3(); const tempVec3b$4 = math.vec3(); const tempVec3c$3 = math.vec3(); const tempVec3d = math.vec3(); const tempCameraTarget = { eye: math.vec3(), look: math.vec3(), up: math.vec3() }; /** * @private */ class KeyboardAxisViewHandler { constructor(scene, controllers, configs, states) { this._scene = scene; const cameraControl = controllers.cameraControl; const camera = scene.camera; this._onSceneKeyDown = scene.input.on("keydown", () => { if (!(configs.active && configs.pointerEnabled) || (!scene.input.keyboardEnabled)) { return; } if (configs.keyboardEnabledOnlyIfMouseover && !states.mouseover) { return; } const axisViewRight = cameraControl._isKeyDownForAction(cameraControl.AXIS_VIEW_RIGHT); const axisViewBack = cameraControl._isKeyDownForAction(cameraControl.AXIS_VIEW_BACK); const axisViewLeft = cameraControl._isKeyDownForAction(cameraControl.AXIS_VIEW_LEFT); const axisViewFront = cameraControl._isKeyDownForAction(cameraControl.AXIS_VIEW_FRONT); const axisViewTop = cameraControl._isKeyDownForAction(cameraControl.AXIS_VIEW_TOP); const axisViewBottom = cameraControl._isKeyDownForAction(cameraControl.AXIS_VIEW_BOTTOM); if ((!axisViewRight) && (!axisViewBack) && (!axisViewLeft) && (!axisViewFront) && (!axisViewTop) && (!axisViewBottom)) { return; } const aabb = scene.aabb; const diag = math.getAABB3Diag(aabb); math.getAABB3Center(aabb, center); const perspectiveDist = Math.abs(diag / Math.tan(controllers.cameraFlight.fitFOV * math.DEGTORAD)); const orthoScale = diag * 1.1; tempCameraTarget.orthoScale = orthoScale; if (axisViewRight) { tempCameraTarget.eye.set(math.addVec3(center, math.mulVec3Scalar(camera.worldRight, perspectiveDist, tempVec3a$7), tempVec3d)); tempCameraTarget.look.set(center); tempCameraTarget.up.set(camera.worldUp); } else if (axisViewBack) { tempCameraTarget.eye.set(math.addVec3(center, math.mulVec3Scalar(camera.worldForward, perspectiveDist, tempVec3a$7), tempVec3d)); tempCameraTarget.look.set(center); tempCameraTarget.up.set(camera.worldUp); } else if (axisViewLeft) { tempCameraTarget.eye.set(math.addVec3(center, math.mulVec3Scalar(camera.worldRight, -perspectiveDist, tempVec3a$7), tempVec3d)); tempCameraTarget.look.set(center); tempCameraTarget.up.set(camera.worldUp); } else if (axisViewFront) { tempCameraTarget.eye.set(math.addVec3(center, math.mulVec3Scalar(camera.worldForward, -perspectiveDist, tempVec3a$7), tempVec3d)); tempCameraTarget.look.set(center); tempCameraTarget.up.set(camera.worldUp); } else if (axisViewTop) { tempCameraTarget.eye.set(math.addVec3(center, math.mulVec3Scalar(camera.worldUp, perspectiveDist, tempVec3a$7), tempVec3d)); tempCameraTarget.look.set(center); tempCameraTarget.up.set(math.normalizeVec3(math.mulVec3Scalar(camera.worldForward, 1, tempVec3b$4), tempVec3c$3)); } else if (axisViewBottom) { tempCameraTarget.eye.set(math.addVec3(center, math.mulVec3Scalar(camera.worldUp, -perspectiveDist, tempVec3a$7), tempVec3d)); tempCameraTarget.look.set(center); tempCameraTarget.up.set(math.normalizeVec3(math.mulVec3Scalar(camera.worldForward, -1, tempVec3b$4))); } if ((!configs.firstPerson) && configs.followPointer) { controllers.pivotController.setPivotPos(center); } if (controllers.cameraFlight.duration > 0) { controllers.cameraFlight.flyTo(tempCameraTarget, () => { if (controllers.pivotController.getPivoting() && configs.followPointer) { controllers.pivotController.showPivot(); } }); } else { controllers.cameraFlight.jumpTo(tempCameraTarget); if (controllers.pivotController.getPivoting() && configs.followPointer) { controllers.pivotController.showPivot(); } } }); } reset() { } destroy() { this._scene.input.off(this._onSceneKeyDown); } } /** * @private */ class MousePickHandler { constructor(scene, controllers, configs, states, updates) { this._scene = scene; const pickController = controllers.pickController; const pivotController = controllers.pivotController; const cameraControl = controllers.cameraControl; this._clicks = 0; this._timeout = null; this._lastPickedEntityId = null; this._lastClickedWorldPos = null; let leftDown = false; let rightDown = false; const canvas = this._scene.canvas.canvas; const flyCameraTo = (pickResult) => { let pos; if (pickResult && pickResult.worldPos) { pos = pickResult.worldPos; } const aabb = pickResult && pickResult.entity ? pickResult.entity.aabb : scene.aabb; if (pos) { // Fly to look at point, don't change eye->look dist const camera = scene.camera; math.subVec3(camera.eye, camera.look, []); controllers.cameraFlight.flyTo({ // look: pos, // eye: xeokit.math.addVec3(pos, diff, []), // up: camera.up, aabb: aabb }); // TODO: Option to back off to fit AABB in view } else {// Fly to fit target boundary in view controllers.cameraFlight.flyTo({ aabb: aabb }); } }; const tickifiedMouseMoveFn = scene.tickify ( this._canvasMouseMoveHandler = (e) => { if (!(configs.active && configs.pointerEnabled)) { return; } if (leftDown || rightDown) { return; } if (cameraControl.hasSubs("rayMove")) { const origin = math.vec3(); const direction = math.vec3(); math.canvasPosToWorldRay(scene.canvas.canvas, scene.camera.viewMatrix, scene.camera.projMatrix, scene.camera.projection, states.pointerCanvasPos, origin, direction); cameraControl.fire("rayMove", { canvasPos: states.pointerCanvasPos, ray: { origin: origin, direction: direction, canvasPos: states.pointerCanvasPos } }, true); } const hoverSubs = cameraControl.hasSubs("hover"); const hoverEnterSubs = cameraControl.hasSubs("hoverEnter"); const hoverOutSubs = cameraControl.hasSubs("hoverOut"); const hoverOffSubs = cameraControl.hasSubs("hoverOff"); const hoverSurfaceSubs = cameraControl.hasSubs("hoverSurface"); const hoverSnapOrSurfaceSubs = cameraControl.hasSubs("hoverSnapOrSurface"); if (hoverSubs || hoverEnterSubs || hoverOutSubs || hoverOffSubs || hoverSurfaceSubs || hoverSnapOrSurfaceSubs) { pickController.pickCursorPos = states.pointerCanvasPos; pickController.schedulePickEntity = true; pickController.schedulePickSurface = hoverSurfaceSubs; pickController.scheduleSnapOrPick = hoverSnapOrSurfaceSubs; pickController.update(); if (pickController.pickResult) { if (pickController.pickResult.entity) { const pickedEntityId = pickController.pickResult.entity.id; if (this._lastPickedEntityId !== pickedEntityId) { if (this._lastPickedEntityId !== undefined) { cameraControl.fire("hoverOut", { // Hovered off an entity entity: scene.objects[this._lastPickedEntityId] }, true); } cameraControl.fire("hoverEnter", pickController.pickResult, true); // Hovering over a new entity this._lastPickedEntityId = pickedEntityId; } } cameraControl.fire("hover", pickController.pickResult, true); if (pickController.pickResult.worldPos || pickController.pickResult.snappedWorldPos) { // Hovering the surface of an entity cameraControl.fire("hoverSurface", pickController.pickResult, true); } } else { if (this._lastPickedEntityId !== undefined) { cameraControl.fire("hoverOut", { // Hovered off an entity entity: scene.objects[this._lastPickedEntityId] }, true); this._lastPickedEntityId = undefined; } cameraControl.fire("hoverOff", { // Not hovering on any entity canvasPos: pickController.pickCursorPos }, true); } } } ); canvas.addEventListener("mousemove", tickifiedMouseMoveFn); canvas.addEventListener('mousedown', this._canvasMouseDownHandler = (e) => { if (e.which === 1) { leftDown = true; } if (e.which === 3) { rightDown = true; } const leftButtonDown = (e.which === 1); if (!leftButtonDown) { return; } if (!(configs.active && configs.pointerEnabled)) { return; } // Left mouse button down to start pivoting states.mouseDownClientX = e.clientX; states.mouseDownClientY = e.clientY; states.mouseDownCursorX = states.pointerCanvasPos[0]; states.mouseDownCursorY = states.pointerCanvasPos[1]; if ((!configs.firstPerson) && configs.followPointer) { pickController.pickCursorPos = states.pointerCanvasPos; pickController.schedulePickSurface = true; pickController.update(); if (e.which === 1) {// Left button const pickResult = pickController.pickResult; if (pickResult && pickResult.worldPos) { pivotController.setPivotPos(pickResult.worldPos); pivotController.startPivot(); this._lastClickedWorldPos = pickResult.worldPos.slice(); } else { if (configs.smartPivot) { pivotController.setCanvasPivotPos(states.pointerCanvasPos); } else { if (this._lastClickedWorldPos) { pivotController.setPivotPos(this._lastClickedWorldPos); } else { pivotController.setPivotPos(scene.camera.look); } } pivotController.startPivot(); } } } }); document.addEventListener('mouseup', this._documentMouseUpHandler = (e) => { if (e.which === 1) { leftDown = false; } if (e.which === 3) { rightDown = false; } if (pivotController.getPivoting()) { pivotController.endPivot(); } }); canvas.addEventListener('mouseup', this._canvasMouseUpHandler = (e) => { if (!(configs.active && configs.pointerEnabled)) { return; } const leftButtonUp = (e.which === 1); if (!leftButtonUp) { return; } // Left mouse button up to possibly pick or double-pick pivotController.hidePivot(); if (Math.abs(e.clientX - states.mouseDownClientX) > 3 || Math.abs(e.clientY - states.mouseDownClientY) > 3) { return; } const pickedSubs = cameraControl.hasSubs("picked"); const pickedNothingSubs = cameraControl.hasSubs("pickedNothing"); const pickedSurfaceSubs = cameraControl.hasSubs("pickedSurface"); const doublePickedSubs = cameraControl.hasSubs("doublePicked"); const doublePickedSurfaceSubs = cameraControl.hasSubs("doublePickedSurface"); const doublePickedNothingSubs = cameraControl.hasSubs("doublePickedNothing"); if ((!configs.doublePickFlyTo) && (!doublePickedSubs) && (!doublePickedSurfaceSubs) && (!doublePickedNothingSubs)) { // Avoid the single/double click differentiation timeout if (pickedSubs || pickedNothingSubs || pickedSurfaceSubs) { pickController.pickCursorPos = states.pointerCanvasPos; pickController.schedulePickEntity = true; pickController.schedulePickSurface = pickedSurfaceSubs; pickController.update(); if (pickController.pickResult) { cameraControl.fire("picked", pickController.pickResult, true); if (pickController.pickedSurface) { cameraControl.fire("pickedSurface", pickController.pickResult, true); } } else { cameraControl.fire("pickedNothing", { canvasPos: states.pointerCanvasPos }, true); } } this._clicks = 0; return; } this._clicks++; if (this._clicks === 1) { // First click pickController.pickCursorPos = states.pointerCanvasPos; pickController.schedulePickEntity = configs.doublePickFlyTo; pickController.schedulePickSurface = pickedSurfaceSubs; pickController.update(); const firstClickPickResult = pickController.pickResult; const firstClickPickSurface = pickController.pickedSurface; this._timeout = setTimeout(() => { if (firstClickPickResult) { cameraControl.fire("picked", firstClickPickResult, true); if (firstClickPickSurface) { cameraControl.fire("pickedSurface", firstClickPickResult, true); if ((!configs.firstPerson) && configs.followPointer) { controllers.pivotController.setPivotPos(firstClickPickResult.worldPos); if (controllers.pivotController.startPivot()) { controllers.pivotController.showPivot(); } } } } else { cameraControl.fire("pickedNothing", { canvasPos: states.pointerCanvasPos }, true); } this._clicks = 0; }, configs.doubleClickTimeFrame); } else { // Second click if (this._timeout !== null) { window.clearTimeout(this._timeout); this._timeout = null; } pickController.pickCursorPos = states.pointerCanvasPos; pickController.schedulePickEntity = configs.doublePickFlyTo || doublePickedSubs || doublePickedSurfaceSubs; pickController.schedulePickSurface = pickController.schedulePickEntity && doublePickedSurfaceSubs; pickController.update(); if (pickController.pickResult) { cameraControl.fire("doublePicked", pickController.pickResult, true); if (pickController.pickedSurface) { cameraControl.fire("doublePickedSurface", pickController.pickResult, true); } if (configs.doublePickFlyTo) { flyCameraTo(pickController.pickResult); if ((!configs.firstPerson) && configs.followPointer) { const pickedEntityAABB = pickController.pickResult.entity.aabb; const pickedEntityCenterPos = math.getAABB3Center(pickedEntityAABB); controllers.pivotController.setPivotPos(pickedEntityCenterPos); if (controllers.pivotController.startPivot()) { controllers.pivotController.showPivot(); } } } } else { cameraControl.fire("doublePickedNothing", { canvasPos: states.pointerCanvasPos }, true); if (configs.doublePickFlyTo) { flyCameraTo(); if ((!configs.firstPerson) && configs.followPointer) { const sceneAABB = scene.aabb; const sceneCenterPos = math.getAABB3Center(sceneAABB); controllers.pivotController.setPivotPos(sceneCenterPos); if (controllers.pivotController.startPivot()) { controllers.pivotController.showPivot(); } } } } this._clicks = 0; } }, false); } reset() { this._clicks = 0; this._lastPickedEntityId = null; if (this._timeout) { window.clearTimeout(this._timeout); this._timeout = null; } } destroy() { const canvas = this._scene.canvas.canvas; canvas.removeEventListener("mousemove", this._canvasMouseMoveHandler); canvas.removeEventListener("mousedown", this._canvasMouseDownHandler); document.removeEventListener("mouseup", this._documentMouseUpHandler); canvas.removeEventListener("mouseup", this._canvasMouseUpHandler); if (this._timeout) { window.clearTimeout(this._timeout); this._timeout = null; } } } /** * @private */ class KeyboardPanRotateDollyHandler { constructor(scene, controllers, configs, states, updates) { this._scene = scene; const input = scene.input; const keyDownMap = []; scene.canvas.canvas; let mouseMovedSinceLastKeyboardDolly = true; this._onSceneMouseMove = input.on("mousemove", () => { mouseMovedSinceLastKeyboardDolly = true; }); this._onSceneKeyDown = input.on("keydown", (keyCode) => { if (!(configs.active && configs.pointerEnabled) || (!scene.input.keyboardEnabled)) { return; } if (configs.keyboardEnabledOnlyIfMouseover && !states.mouseover) { return; } keyDownMap[keyCode] = true; }); this._onSceneKeyUp = input.on("keyup", (keyCode) => { if (!(configs.active && configs.pointerEnabled) || (!scene.input.keyboardEnabled)) { return; } keyDownMap[keyCode] = false; if (controllers.pivotController.getPivoting()) { controllers.pivotController.endPivot(); } }); document.addEventListener("visibilitychange", this._onVisibilityChange = () => { keyDownMap.splice(0); }); window.addEventListener("blur", this._onBlur = () => { keyDownMap.splice(0); }); this._onTick = scene.on("tick", (e) => { if (!(configs.active && configs.pointerEnabled) || (!scene.input.keyboardEnabled)) { return; } if (configs.keyboardEnabledOnlyIfMouseover && !states.mouseover) { return; } const cameraControl = controllers.cameraControl; const elapsedSecs = (e.deltaTime / 1000.0); //------------------------------------------------------------------------------------------------- // Keyboard rotation //------------------------------------------------------------------------------------------------- if (!configs.planView) { const rotateYPos = cameraControl._isKeyDownForAction(cameraControl.ROTATE_Y_POS, keyDownMap); const rotateYNeg = cameraControl._isKeyDownForAction(cameraControl.ROTATE_Y_NEG, keyDownMap); const rotateXPos = cameraControl._isKeyDownForAction(cameraControl.ROTATE_X_POS, keyDownMap); const rotateXNeg = cameraControl._isKeyDownForAction(cameraControl.ROTATE_X_NEG, keyDownMap); const orbitDelta = elapsedSecs * configs.keyboardRotationRate; if (rotateYPos || rotateYNeg || rotateXPos || rotateXNeg) { if ((!configs.firstPerson) && configs.followPointer) { controllers.pivotController.startPivot(); } if (rotateYPos) { updates.rotateDeltaY += orbitDelta; } else if (rotateYNeg) { updates.rotateDeltaY -= orbitDelta; } if (rotateXPos) { updates.rotateDeltaX += orbitDelta; } else if (rotateXNeg) { updates.rotateDeltaX -= orbitDelta; } if ((!configs.firstPerson) && configs.followPointer) { controllers.pivotController.startPivot(); } } } //------------------------------------------------------------------------------------------------- // Keyboard panning //------------------------------------------------------------------------------------------------- if (!keyDownMap[input.KEY_CTRL] && !keyDownMap[input.KEY_ALT]) { const dollyBackwards = cameraControl._isKeyDownForAction(cameraControl.DOLLY_BACKWARDS, keyDownMap); const dollyForwards = cameraControl._isKeyDownForAction(cameraControl.DOLLY_FORWARDS, keyDownMap); if (dollyBackwards || dollyForwards) { const dollyDelta = elapsedSecs * configs.keyboardDollyRate; if ((!configs.firstPerson) && configs.followPointer) { controllers.pivotController.startPivot(); } if (dollyForwards) { updates.dollyDelta -= dollyDelta; } else if (dollyBackwards) { updates.dollyDelta += dollyDelta; } if (mouseMovedSinceLastKeyboardDolly) { states.followPointerDirty = true; mouseMovedSinceLastKeyboardDolly = false; } } } const panForwards = cameraControl._isKeyDownForAction(cameraControl.PAN_FORWARDS, keyDownMap); const panBackwards = cameraControl._isKeyDownForAction(cameraControl.PAN_BACKWARDS, keyDownMap); const panLeft = cameraControl._isKeyDownForAction(cameraControl.PAN_LEFT, keyDownMap); const panRight = cameraControl._isKeyDownForAction(cameraControl.PAN_RIGHT, keyDownMap); const panUp = cameraControl._isKeyDownForAction(cameraControl.PAN_UP, keyDownMap); const panDown = cameraControl._isKeyDownForAction(cameraControl.PAN_DOWN, keyDownMap); const panDelta = (keyDownMap[input.KEY_ALT] ? 0.3 : 1.0) * elapsedSecs * configs.keyboardPanRate; // ALT for slower pan rate if (panForwards || panBackwards || panLeft || panRight || panUp || panDown) { if ((!configs.firstPerson) && configs.followPointer) { controllers.pivotController.startPivot(); } if (panDown) { updates.panDeltaY += panDelta; } else if (panUp) { updates.panDeltaY += -panDelta; } if (panRight) { updates.panDeltaX += -panDelta; } else if (panLeft) { updates.panDeltaX += panDelta; } if (panBackwards) { updates.panDeltaZ += panDelta; } else if (panForwards) { updates.panDeltaZ += -panDelta; } } }); } reset() { } destroy() { this._scene.off(this._onTick); this._scene.input.off(this._onSceneMouseMove); this._scene.input.off(this._onSceneKeyDown); document.removeEventListener("visibilitychange", this._onVisibilityChange); window.removeEventListener("blur", this._onBlur); this._scene.input.off(this._onSceneKeyUp); } } const SCALE_DOLLY_EACH_FRAME = 1; // Recalculate dolly speed for eye->target distance on each Nth frame const EPSILON = 0.001; const tempVec3$3 = math.vec3(); /** * Handles camera updates on each "tick" that were scheduled by the various controllers. * * @private */ class CameraUpdater { constructor(scene, controllers, configs, states, updates) { this._scene = scene; const camera = scene.camera; const pickController = controllers.pickController; const pivotController = controllers.pivotController; const panController = controllers.panController; const cameraControl = controllers.cameraControl; let countDown = SCALE_DOLLY_EACH_FRAME; // Decrements on each tick let dollyDistFactor = 1.0; // Calculated when countDown is zero let followPointerWorldPos = null; // Holds the pointer's World position when configs.followPointer is true this._onTick = scene.on("tick", () => { if (!(configs.active && configs.pointerEnabled)) { return; } let cursorType = "default"; //---------------------------------------------------------------------------------------------------------- // Dolly decay //------------------------------------------------------------------------------------ ---------------------- if (Math.abs(updates.dollyDelta) < EPSILON) { updates.dollyDelta = 0; } //---------------------------------------------------------------------------------------------------------- // Rotation decay //---------------------------------------------------------------------------------------------------------- if (Math.abs(updates.rotateDeltaX) < EPSILON) { updates.rotateDeltaX = 0; } if (Math.abs(updates.rotateDeltaY) < EPSILON) { updates.rotateDeltaY = 0; } if (updates.rotateDeltaX !== 0 || updates.rotateDeltaY !== 0) { updates.dollyDelta = 0; } //---------------------------------------------------------------------------------------------------------- // Dolly speed eye->look scaling // // If pointer is over an object, then dolly speed is proportional to the distance to that object. // // If pointer is not over an object, then dolly speed is proportional to the distance to the last // object the pointer was over. This is so that we can dolly to structures that may have gaps through // which empty background shows, that the pointer may inadvertently be over. In these cases, we don't // want dolly speed wildly varying depending on how accurately the user avoids the gaps with the pointer. //---------------------------------------------------------------------------------------------------------- if (configs.followPointer) { if (--countDown <= 0) { countDown = SCALE_DOLLY_EACH_FRAME; if (updates.dollyDelta !== 0) { if (updates.rotateDeltaY === 0 && updates.rotateDeltaX === 0) { if (configs.followPointer && states.followPointerDirty) { pickController.pickCursorPos = states.pointerCanvasPos; pickController.schedulePickSurface = true; pickController.update(); if (pickController.pickResult && pickController.pickResult.worldPos) { followPointerWorldPos = pickController.pickResult.worldPos; } else { dollyDistFactor = 1.0; followPointerWorldPos = null; } states.followPointerDirty = false; } } if (followPointerWorldPos) { const dist = Math.abs(math.lenVec3(math.subVec3(followPointerWorldPos, scene.camera.eye, tempVec3$3))); dollyDistFactor = dist / configs.dollyProximityThreshold; } if (dollyDistFactor < configs.dollyMinSpeed) { dollyDistFactor = configs.dollyMinSpeed; } } } } else { dollyDistFactor = 1; followPointerWorldPos = null; } const dollyDeltaForDist = (updates.dollyDelta * dollyDistFactor); //---------------------------------------------------------------------------------------------------------- // Rotation //---------------------------------------------------------------------------------------------------------- if (updates.rotateDeltaY !== 0 || updates.rotateDeltaX !== 0) { if ((!configs.firstPerson) && configs.followPointer && pivotController.getPivoting()) { pivotController.continuePivot(updates.rotateDeltaY, updates.rotateDeltaX); pivotController.showPivot(); } else { if (updates.rotateDeltaX !== 0) { if (configs.firstPerson) { camera.pitch(-updates.rotateDeltaX); } else { camera.orbitPitch(updates.rotateDeltaX); } } if (updates.rotateDeltaY !== 0) { if (configs.firstPerson) { camera.yaw(updates.rotateDeltaY); } else { camera.orbitYaw(updates.rotateDeltaY); } } } updates.rotateDeltaX *= configs.rotationInertia; updates.rotateDeltaY *= configs.rotationInertia; cursorType = cameraControl._cursors.rotate; } //---------------------------------------------------------------------------------------------------------- // Panning //---------------------------------------------------------------------------------------------------------- if (Math.abs(updates.panDeltaX) < EPSILON) { updates.panDeltaX = 0; } if (Math.abs(updates.panDeltaY) < EPSILON) { updates.panDeltaY = 0; } if (Math.abs(updates.panDeltaZ) < EPSILON) { updates.panDeltaZ = 0; } if (updates.panDeltaX !== 0 || updates.panDeltaY !== 0 || updates.panDeltaZ !== 0) { const vec = math.vec3(); vec[0] = updates.panDeltaX; vec[1] = updates.panDeltaY; vec[2] = updates.panDeltaZ; let verticalEye; let verticalLook; if (configs.constrainVertical) { if (camera.xUp) { verticalEye = camera.eye[0]; verticalLook = camera.look[0]; } else if (camera.yUp) { verticalEye = camera.eye[1]; verticalLook = camera.look[1]; } else if (camera.zUp) { verticalEye = camera.eye[2]; verticalLook = camera.look[2]; } camera.pan(vec); const eye = camera.eye; const look = camera.look; if (camera.xUp) { eye[0] = verticalEye; look[0] = verticalLook; } else if (camera.yUp) { eye[1] = verticalEye; look[1] = verticalLook; } else if (camera.zUp) { eye[2] = verticalEye; look[2] = verticalLook; } camera.eye = eye; camera.look = look; } else { camera.pan(vec); } cursorType = cameraControl._cursors.pan; } updates.panDeltaX *= configs.panInertia; updates.panDeltaY *= configs.panInertia; updates.panDeltaZ *= configs.panInertia; //---------------------------------------------------------------------------------------------------------- // Dollying //---------------------------------------------------------------------------------------------------------- if (dollyDeltaForDist !== 0) { const isOrtho = camera.projection === "ortho"; if (dollyDeltaForDist < 0) { cursorType = cameraControl._cursors.dollyForward; } else { cursorType = cameraControl._cursors.dollyBackward; } if (configs.firstPerson) { let verticalEye; let verticalLook; if (configs.constrainVertical) { if (camera.xUp) { verticalEye = camera.eye[0]; verticalLook = camera.look[0]; } else if (camera.yUp) { verticalEye = camera.eye[1]; verticalLook = camera.look[1]; } else if (camera.zUp) { verticalEye = camera.eye[2]; verticalLook = camera.look[2]; } } if (configs.followPointer) { const dolliedThroughSurface = panController.dollyToCanvasPos(followPointerWorldPos, states.pointerCanvasPos, -dollyDeltaForDist); if (dolliedThroughSurface) { states.followPointerDirty = true; } } else if (isOrtho) { camera.ortho.scale = camera.ortho.scale - dollyDeltaForDist; } else { camera.pan([0, 0, dollyDeltaForDist]); } if (configs.constrainVertical) { const eye = camera.eye; const look = camera.look; if (camera.xUp) { eye[0] = verticalEye; look[0] = verticalLook; } else if (camera.yUp) { eye[1] = verticalEye; look[1] = verticalLook; } else if (camera.zUp) { eye[2] = verticalEye; look[2] = verticalLook; } camera.eye = eye; camera.look = look; } } else if (configs.planView) { if (configs.followPointer) { const dolliedThroughSurface = panController.dollyToCanvasPos(followPointerWorldPos, states.pointerCanvasPos, -dollyDeltaForDist); if (dolliedThroughSurface) { states.followPointerDirty = true; } } else if (isOrtho) { camera.ortho.scale = camera.ortho.scale + dollyDeltaForDist; } else { camera.zoom(dollyDeltaForDist); } } else { // Orbiting if (configs.followPointer) { const dolliedThroughSurface = panController.dollyToCanvasPos(followPointerWorldPos, states.pointerCanvasPos, -dollyDeltaForDist); if (dolliedThroughSurface) { states.followPointerDirty = true; } } else if (isOrtho) { camera.ortho.scale = camera.ortho.scale + dollyDeltaForDist; } else { camera.zoom(dollyDeltaForDist); } } updates.dollyDelta *= configs.dollyInertia; } pickController.fireEvents(); document.body.style.cursor = cursorType; }); } destroy() { this._scene.off(this._onTick); } } /** * @private */ class MouseMiscHandler { constructor(scene, controllers, configs, states, updates) { this._scene = scene; const canvas = this._scene.canvas.canvas; canvas.addEventListener("mouseenter", this._mouseEnterHandler = () => { states.mouseover = true; }); canvas.addEventListener("mouseleave", this._mouseLeaveHandler = () => { states.mouseover = false; canvas.style.cursor = null; }); document.addEventListener("mousemove", this._mouseMoveHandler = (e) => { getCanvasPosFromEvent$2(e, canvas, states.pointerCanvasPos); }); canvas.addEventListener("mousedown", this._mouseDownHandler = (e) => { if (!(configs.active && configs.pointerEnabled)) { return; } getCanvasPosFromEvent$2(e, canvas, states.pointerCanvasPos); states.mouseover = true; }); canvas.addEventListener("mouseup", this._mouseUpHandler = (e) => { if (!(configs.active && configs.pointerEnabled)) { return; } }); } reset() { } destroy() { const canvas = this._scene.canvas.canvas; document.removeEventListener("mousemove", this._mouseMoveHandler); canvas.removeEventListener("mouseenter", this._mouseEnterHandler); canvas.removeEventListener("mouseleave", this._mouseLeaveHandler); canvas.removeEventListener("mousedown", this._mouseDownHandler); canvas.removeEventListener("mouseup", this._mouseUpHandler); } } function getCanvasPosFromEvent$2(event, canvas, canvasPos) { if (!event) { event = window.event; canvasPos[0] = event.x; canvasPos[1] = event.y; } else { const { left, top } = canvas.getBoundingClientRect(); canvasPos[0] = event.clientX - left; canvasPos[1] = event.clientY - top; } return canvasPos; } const getCanvasPosFromEvent$1 = function (event, canvasPos) { if (!event) { event = window.event; canvasPos[0] = event.x; canvasPos[1] = event.y; } else { let element = event.target; let totalOffsetLeft = 0; let totalOffsetTop = 0; while (element.offsetParent) { totalOffsetLeft += element.offsetLeft; totalOffsetTop += element.offsetTop; element = element.offsetParent; } canvasPos[0] = event.pageX - totalOffsetLeft; canvasPos[1] = event.pageY - totalOffsetTop; } return canvasPos; }; /** * @private */ class TouchPanRotateAndDollyHandler { constructor(scene, controllers, configs, states, updates) { this._scene = scene; const pickController = controllers.pickController; const pivotController = controllers.pivotController; const tapStartCanvasPos = math.vec2(); const tapCanvasPos0 = math.vec2(); const tapCanvasPos1 = math.vec2(); const touch0Vec = math.vec2(); const lastCanvasTouchPosList = []; const canvas = this._scene.canvas.canvas; let numTouches = 0; let waitForTick = false; this._onTick = scene.on("tick", () => { waitForTick = false; }); canvas.addEventListener("touchstart", this._canvasTouchStartHandler = (event) => { if (!(configs.active && configs.pointerEnabled)) { return; } event.preventDefault(); const touches = event.touches; const changedTouches = event.changedTouches; states.touchStartTime = Date.now(); if (touches.length === 1 && changedTouches.length === 1) { getCanvasPosFromEvent$1(touches[0], tapStartCanvasPos); if (configs.followPointer) { pickController.pickCursorPos = tapStartCanvasPos; pickController.schedulePickSurface = true; pickController.update(); if (!configs.planView) { if (pickController.picked && pickController.pickedSurface && pickController.pickResult && pickController.pickResult.worldPos) { pivotController.setPivotPos(pickController.pickResult.worldPos); if (!configs.firstPerson && pivotController.startPivot()) { pivotController.showPivot(); } } else { if (configs.smartPivot) { pivotController.setCanvasPivotPos(states.pointerCanvasPos); } else { pivotController.setPivotPos(scene.camera.look); } if (!configs.firstPerson && pivotController.startPivot()) { pivotController.showPivot(); } } } } } while (lastCanvasTouchPosList.length < touches.length) { lastCanvasTouchPosList.push(math.vec2()); } for (let i = 0, len = touches.length; i < len; ++i) { getCanvasPosFromEvent$1(touches[i], lastCanvasTouchPosList[i]); } numTouches = touches.length; }); canvas.addEventListener("touchend", this._canvasTouchEndHandler = () => { if (pivotController.getPivoting()) { pivotController.endPivot(); pivotController.hidePivot(); } }); canvas.addEventListener("touchmove", this._canvasTouchMoveHandler = (event) => { if (!(configs.active && configs.pointerEnabled)) { return; } event.stopPropagation(); event.preventDefault(); if (waitForTick) { // Limit changes detection to one per frame return; } waitForTick = true; // Scaling drag-rotate to canvas boundary const canvasBoundary = scene.canvas.boundary; const canvasWidth = canvasBoundary[2]; const canvasHeight = canvasBoundary[3]; const touches = event.touches; if (event.touches.length !== numTouches) { // Two fingers were pressed, then one of them is removed // We don't want to rotate in this case (weird behavior) return; } if (numTouches === 1) { getCanvasPosFromEvent$1(touches[0], tapCanvasPos0); //----------------------------------------------------------------------------------------------- // Drag rotation //----------------------------------------------------------------------------------------------- math.subVec2(tapCanvasPos0, lastCanvasTouchPosList[0], touch0Vec); const xPanDelta = touch0Vec[0]; const yPanDelta = touch0Vec[1]; if (states.longTouchTimeout !== null && (Math.abs(xPanDelta) > configs.longTapRadius || Math.abs(yPanDelta) > configs.longTapRadius)) { clearTimeout(states.longTouchTimeout); states.longTouchTimeout = null; } if (configs.planView) { // No rotating in plan-view mode const camera = scene.camera; // We use only canvasHeight here so that aspect ratio does not distort speed if (camera.projection === "perspective") { const depth = Math.abs(scene.camera.eyeLookDist); const targetDistance = depth * Math.tan((camera.perspective.fov / 2) * Math.PI / 180.0); updates.panDeltaX += (xPanDelta * targetDistance / canvasHeight) * configs.touchPanRate; updates.panDeltaY += (yPanDelta * targetDistance / canvasHeight) * configs.touchPanRate; } else { updates.panDeltaX += 0.5 * camera.ortho.scale * (xPanDelta / canvasHeight) * configs.touchPanRate; updates.panDeltaY += 0.5 * camera.ortho.scale * (yPanDelta / canvasHeight) * configs.touchPanRate; } } else { // if (!absorbTinyFirstDrag) { updates.rotateDeltaY -= (xPanDelta / canvasWidth) * (configs.dragRotationRate * 1.0); // Full horizontal rotation updates.rotateDeltaX += (yPanDelta / canvasHeight) * (configs.dragRotationRate * 1.5); // Half vertical rotation // } else { // firstDragDeltaY -= (xPanDelta / canvasWidth) * (configs.dragRotationRate * 1.0); // Full horizontal rotation // firstDragDeltaX += (yPanDelta / canvasHeight) * (configs.dragRotationRate * 1.5); // Half vertical rotation // if (Math.abs(firstDragDeltaX) > 5 || Math.abs(firstDragDeltaY) > 5) { // updates.rotateDeltaX += firstDragDeltaX; // updates.rotateDeltaY += firstDragDeltaY; // firstDragDeltaX = 0; // firstDragDeltaY = 0; // absorbTinyFirstDrag = false; // } // } } } else if (numTouches === 2) { const touch0 = touches[0]; const touch1 = touches[1]; getCanvasPosFromEvent$1(touch0, tapCanvasPos0); getCanvasPosFromEvent$1(touch1, tapCanvasPos1); const lastMiddleTouch = math.geometricMeanVec2(lastCanvasTouchPosList[0], lastCanvasTouchPosList[1]); const currentMiddleTouch = math.geometricMeanVec2(tapCanvasPos0, tapCanvasPos1); const touchDelta = math.vec2(); math.subVec2(lastMiddleTouch, currentMiddleTouch, touchDelta); const xPanDelta = touchDelta[0]; const yPanDelta = touchDelta[1]; const camera = scene.camera; // Dollying const d1 = math.distVec2([touch0.pageX, touch0.pageY], [touch1.pageX, touch1.pageY]); const d2 = math.distVec2(lastCanvasTouchPosList[0], lastCanvasTouchPosList[1]); const dollyDelta = (d2 - d1) * configs.touchDollyRate; updates.dollyDelta = dollyDelta; if (Math.abs(dollyDelta) < 1.0) { // We use only canvasHeight here so that aspect ratio does not distort speed if (camera.projection === "perspective") { const pickedWorldPos = pickController.pickResult ? pickController.pickResult.worldPos : scene.center; const depth = Math.abs(math.lenVec3(math.subVec3(pickedWorldPos, scene.camera.eye, []))); const targetDistance = depth * Math.tan((camera.perspective.fov / 2) * Math.PI / 180.0); updates.panDeltaX -= (xPanDelta * targetDistance / canvasHeight) * configs.touchPanRate; updates.panDeltaY -= (yPanDelta * targetDistance / canvasHeight) * configs.touchPanRate; } else { updates.panDeltaX -= 0.5 * camera.ortho.scale * (xPanDelta / canvasHeight) * configs.touchPanRate; updates.panDeltaY -= 0.5 * camera.ortho.scale * (yPanDelta / canvasHeight) * configs.touchPanRate; } } states.pointerCanvasPos = currentMiddleTouch; } for (let i = 0; i < numTouches; ++i) { getCanvasPosFromEvent$1(touches[i], lastCanvasTouchPosList[i]); } }); } reset() { } destroy() { const canvas = this._scene.canvas.canvas; canvas.removeEventListener("touchstart", this._canvasTouchStartHandler); canvas.removeEventListener("touchend", this._canvasTouchEndHandler); canvas.removeEventListener("touchmove", this._canvasTouchMoveHandler); this._scene.off(this._onTick); } } const TAP_INTERVAL = 150; const DBL_TAP_INTERVAL = 325; const TAP_DISTANCE_THRESHOLD = 4; const getCanvasPosFromEvent = function (event, canvasPos) { if (!event) { event = window.event; canvasPos[0] = event.x; canvasPos[1] = event.y; } else { let element = event.target; let totalOffsetLeft = 0; let totalOffsetTop = 0; while (element.offsetParent) { totalOffsetLeft += element.offsetLeft; totalOffsetTop += element.offsetTop; element = element.offsetParent; } canvasPos[0] = event.pageX - totalOffsetLeft; canvasPos[1] = event.pageY - totalOffsetTop; } return canvasPos; }; /** * @private */ class TouchPickHandler { constructor(scene, controllers, configs, states, updates) { this._scene = scene; const pickController = controllers.pickController; const cameraControl = controllers.cameraControl; let touchStartTime; const activeTouches = []; const tapStartPos = new Float32Array(2); let tapStartTime = -1; let lastTapTime = -1; const canvas = this._scene.canvas.canvas; const flyCameraTo = (pickResult) => { let pos; if (pickResult && pickResult.worldPos) { pos = pickResult.worldPos; } const aabb = pickResult ? pickResult.entity.aabb : scene.aabb; if (pos) { // Fly to look at point, don't change eye->look dist const camera = scene.camera; math.subVec3(camera.eye, camera.look, []); controllers.cameraFlight.flyTo({ aabb: aabb }); // TODO: Option to back off to fit AABB in view } else {// Fly to fit target boundary in view controllers.cameraFlight.flyTo({ aabb: aabb }); } }; canvas.addEventListener("touchstart", this._canvasTouchStartHandler = (e) => { if (!(configs.active && configs.pointerEnabled)) { return; } if (states.longTouchTimeout !== null) { clearTimeout(states.longTouchTimeout); states.longTouchTimeout = null; } const touches = e.touches; const changedTouches = e.changedTouches; touchStartTime = Date.now(); if (touches.length === 1 && changedTouches.length === 1) { tapStartTime = touchStartTime; getCanvasPosFromEvent(touches[0], tapStartPos); const rightClickClientX = tapStartPos[0]; const rightClickClientY = tapStartPos[1]; const rightClickPageX = touches[0].pageX; const rightClickPageY = touches[0].pageY; states.longTouchTimeout = setTimeout(() => { controllers.cameraControl.fire("rightClick", { // For context menus pagePos: [Math.round(rightClickPageX), Math.round(rightClickPageY)], canvasPos: [Math.round(rightClickClientX), Math.round(rightClickClientY)], event: e }, true); states.longTouchTimeout = null; }, configs.longTapTimeout); } else { tapStartTime = -1; } while (activeTouches.length < touches.length) { activeTouches.push(new Float32Array(2)); } for (let i = 0, len = touches.length; i < len; ++i) { getCanvasPosFromEvent(touches[i], activeTouches[i]); } activeTouches.length = touches.length; }, {passive: true}); canvas.addEventListener("touchend", this._canvasTouchEndHandler = (e) => { if (!(configs.active && configs.pointerEnabled)) { return; } const currentTime = Date.now(); const touches = e.touches; const changedTouches = e.changedTouches; const pickedSurfaceSubs = cameraControl.hasSubs("pickedSurface"); if (states.longTouchTimeout !== null) { clearTimeout(states.longTouchTimeout); states.longTouchTimeout = null; } // process tap if (touches.length === 0 && changedTouches.length === 1) { if (tapStartTime > -1 && currentTime - tapStartTime < TAP_INTERVAL) { if (lastTapTime > -1 && tapStartTime - lastTapTime < DBL_TAP_INTERVAL) { // Double-tap getCanvasPosFromEvent(changedTouches[0], pickController.pickCursorPos); pickController.schedulePickEntity = true; pickController.schedulePickSurface = pickedSurfaceSubs; pickController.update(); if (pickController.pickResult) { pickController.pickResult.touchInput = true; cameraControl.fire("doublePicked", pickController.pickResult); if (pickController.pickedSurface) { cameraControl.fire("doublePickedSurface", pickController.pickResult); } if (configs.doublePickFlyTo) { flyCameraTo(pickController.pickResult); } } else { cameraControl.fire("doublePickedNothing"); if (configs.doublePickFlyTo) { flyCameraTo(); } } lastTapTime = -1; } else if (math.distVec2(activeTouches[0], tapStartPos) < TAP_DISTANCE_THRESHOLD) { // Single-tap getCanvasPosFromEvent(changedTouches[0], pickController.pickCursorPos); pickController.schedulePickEntity = true; pickController.schedulePickSurface = pickedSurfaceSubs; pickController.update(); if (pickController.pickResult) { pickController.pickResult.touchInput = true; cameraControl.fire("picked", pickController.pickResult); if (pickController.pickedSurface) { cameraControl.fire("pickedSurface", pickController.pickResult); } } else { cameraControl.fire("pickedNothing"); } lastTapTime = currentTime; } tapStartTime = -1; } } activeTouches.length = touches.length; for (let i = 0, len = touches.length; i < len; ++i) { activeTouches[i][0] = touches[i].pageX; activeTouches[i][1] = touches[i].pageY; } // e.stopPropagation(); }, {passive: true}); } reset() { // TODO // tapStartTime = -1; // lastTapTime = -1; } destroy() { const canvas = this._scene.canvas.canvas; canvas.removeEventListener("touchstart", this._canvasTouchStartHandler); canvas.removeEventListener("touchend", this._canvasTouchEndHandler); } } const DEFAULT_SNAP_PICK_RADIUS = 30; const DEFAULT_SNAP_VERTEX = true; const DEFAULT_SNAP_EDGE = true; /** * @desc Controls the {@link Camera} with user input, and fires events when the user interacts with pickable {@link Entity}s. * * # Contents * * * [Overview](#overview) * * [Examples](#examples) * * [Orbit Mode](#orbit-mode) * + [Following the Pointer in Orbit Mode](#--following-the-pointer-in-orbit-mode--) * + [Showing the Pivot Position](#--showing-the-pivot-position--) * + [Axis-Aligned Views in Orbit Mode](#--axis-aligned-views-in-orbit-mode--) * + [View-Fitting Entitys in Orbit Mode](#--view-fitting-entitys-in-orbit-mode--) * * [First-Person Mode](#first-person-mode) * + [Following the Pointer in First-Person Mode](#--following-the-pointer-in-first-person-mode--) * + [Constraining Vertical Position in First-Person Mode](#--constraining-vertical-position-in-first-person-mode--) * + [Axis-Aligned Views in First-Person Mode](#--axis-aligned-views-in-first-person-mode--) * + [View-Fitting Entitys in First-Person Mode](#--view-fitting-entitys-in-first-person-mode--) * * [Plan-View Mode](#plan-view-mode) * + [Following the Pointer in Plan-View Mode](#--following-the-pointer-in-plan-view-mode--) * + [Axis-Aligned Views in Plan-View Mode](#--axis-aligned-views-in-plan-view-mode--) * * [CameraControl Events](#cameracontrol-events) * + ["hover"](#---hover---) * + ["hoverOff"](#---hoveroff---) * + ["hoverEnter"](#---hoverenter---) * + ["hoverOut"](#---hoverout---) * + ["picked"](#---picked---) * + ["pickedSurface"](#---pickedsurface---) * + ["pickedNothing"](#---pickednothing---) * + ["doublePicked"](#---doublepicked---) * + ["doublePickedSurface"](#---doublepickedsurface---) * + ["doublePickedNothing"](#---doublepickednothing---) * + ["rightClick"](#---rightclick---) * * [Custom Keyboard Mappings](#custom-keyboard-mappings) * *

* * # Overview * * * Each {@link Viewer} has a ````CameraControl````, located at {@link Viewer#cameraControl}. * * {@link CameraControl#navMode} selects the navigation mode: * * ````"orbit"```` rotates the {@link Camera} position about the target. * * ````"firstPerson"```` rotates the World about the Camera position. * * ````"planView"```` never rotates, but still allows to pan and dolly, typically for an axis-aligned view. * * {@link CameraControl#followPointer} makes the Camera follow the mouse or touch pointer. * * {@link CameraControl#constrainVertical} locks the Camera to its current height when in first-person mode. * * ````CameraControl```` fires pick events when we hover, click or tap on an {@link Entity}. *

* * # Examples * * * [Orbit Navigation - Duplex Model](https://xeokit.github.io/xeokit-sdk/examples/index.html#CameraControl_orbit_Duplex) * * [Orbit Navigation - Holter Tower Model](https://xeokit.github.io/xeokit-sdk/examples/index.html#CameraControl_orbit_HolterTower) * * [First-Person Navigation - Duplex Model](https://xeokit.github.io/xeokit-sdk/examples/index.html#CameraControl_firstPerson_Duplex) * * [First-Person Navigation - Holter Tower Model](https://xeokit.github.io/xeokit-sdk/examples/index.html#CameraControl_firstPerson_HolterTower) * * [Plan-view Navigation - Schependomlaan Model](https://xeokit.github.io/xeokit-sdk/examples/index.html#CameraControl_planView_Schependomlaan) * * [Custom Keyboard Mapping](https://xeokit.github.io/xeokit-sdk/examples/index.html#CameraControl_keyMap) *

* * # Orbit Mode * * In orbit mode, ````CameraControl```` orbits the {@link Camera} about the target. * * To enable orbit mode: * * ````javascript * const cameraControl = myViewer.cameraControl; * cameraControl.navMode = "orbit"; * ```` * * Then orbit by: * * * left-dragging the mouse, * * tap-dragging the touch pad, and * * pressing arrow keys, or ````Q```` and ````E```` on a QWERTY keyboard, or ````A```` and ````E```` on an AZERTY keyboard. *

* * Dolly forwards and backwards by: * * * spinning the mouse wheel, * * pinching on the touch pad, and * * pressing the ````+```` and ````-```` keys, or ````W```` and ````S```` on a QWERTY keyboard, or ````Z```` and ````S```` for AZERTY. *

* * Pan horizontally and vertically by: * * * right-dragging the mouse, * * left-dragging the mouse with the SHIFT key down, * * tap-dragging the touch pad with SHIFT down, * * pressing the ````A````, ````D````, ````Z```` and ````X```` keys on a QWERTY keyboard, and * * pressing the ````Q````, ````D````, ````W```` and ````X```` keys on an AZERTY keyboard, *

* * ## Following the Pointer in Orbit Mode * * When {@link CameraControl#followPointer} is ````true````in orbiting mode, the mouse or touch pointer will dynamically * indicate the target that the {@link Camera} will orbit, as well as dolly to and from. * * Lets ensure that we're in orbit mode, then enable the {@link Camera} to follow the pointer: * * ````javascript * cameraControl.navMode = "orbit"; * cameraControl.followPointer = true; * ```` * * ## Smart Pivoting * * TODO * * ## Showing the Pivot Position * * We can configure {@link CameraControl#pivotElement} with an HTML element to indicate the current * pivot position. The indicator will appear momentarily each time we move the {@link Camera} while in orbit mode with * {@link CameraControl#followPointer} set ````true````. * * First we'll define some CSS to style our pivot indicator as a black dot with a white border: * * ````css * .camera-pivot-marker { * color: #ffffff; * position: absolute; * width: 25px; * height: 25px; * border-radius: 15px; * border: 2px solid #ebebeb; * background: black; * visibility: hidden; * box-shadow: 5px 5px 15px 1px #000000; * z-index: 10000; * pointer-events: none; * } * ```` * * Then we'll attach our pivot indicator's HTML element to the ````CameraControl````: * * ````javascript * const pivotElement = document.createRange().createContextualFragment("
").firstChild; * * document.body.appendChild(pivotElement); * * cameraControl.pivotElement = pivotElement; * ```` * * ## Axis-Aligned Views in Orbit Mode * * In orbit mode, we can use keys 1-6 to position the {@link Camera} to look at the center of the {@link Scene} from along each of the * six World-space axis. Pressing one of these keys will fly the {@link Camera} to the corresponding axis-aligned view. * * ## View-Fitting Entitys in Orbit Mode * * When {@link CameraControl#doublePickFlyTo} is ````true````, we can left-double-click or * double-tap (ie. "double-pick") an {@link Entity} to fit it to view. This will cause the {@link Camera} * to fly to that Entity. Our target then becomes the center of that Entity. If we are currently pivoting, * then our pivot position is then also set to the Entity center. * * Disable that behaviour by setting {@link CameraControl#doublePickFlyTo} ````false````. * * # First-Person Mode * * In first-person mode, ````CameraControl```` rotates the World about the {@link Camera} position. * * To enable first-person mode: * * ````javascript * cameraControl.navMode = "firstPerson"; * ```` * * Then rotate by: * * * left-dragging the mouse, * * tap-dragging the touch pad, * * pressing arrow keys, or ````Q```` and ````E```` on a QWERTY keyboard, or ````A```` and ````E```` on an AZERTY keyboard. *

* * Dolly forwards and backwards by: * * * spinning the mouse wheel, * * pinching on the touch pad, and * * pressing the ````+```` and ````-```` keys, or ````W```` and ````S```` on a QWERTY keyboard, or ````Z```` and ````S```` for AZERTY. *

* * Pan left, right, up and down by: * * * left-dragging or right-dragging the mouse, and * * tap-dragging the touch pad with SHIFT down. * * Pan forwards, backwards, left, right, up and down by pressing the ````WSADZX```` keys on a QWERTY keyboard, * or ````WSQDWX```` keys on an AZERTY keyboard. *

* * ## Following the Pointer in First-Person Mode * * When {@link CameraControl#followPointer} is ````true```` in first-person mode, the mouse or touch pointer will dynamically * indicate the target to which the {@link Camera} will dolly to and from. In first-person mode, however, the World will always rotate * about the {@link Camera} position. * * Lets ensure that we're in first-person mode, then enable the {@link Camera} to follow the pointer: * * ````javascript * cameraControl.navMode = "firstPerson"; * cameraControl.followPointer = true; * ```` * * When the pointer is over empty space, the target will remain the last object that the pointer was over. * * ## Constraining Vertical Position in First-Person Mode * * In first-person mode, we can lock the {@link Camera} to its current position on the vertical World axis, which is useful for walk-through navigation: * * ````javascript * cameraControl.constrainVertical = true; * ```` * * ## Axis-Aligned Views in First-Person Mode * * In first-person mode we can use keys 1-6 to position the {@link Camera} to look at the center of * the {@link Scene} from along each of the six World-space axis. Pressing one of these keys will fly the {@link Camera} to the * corresponding axis-aligned view. * * ## View-Fitting Entitys in First-Person Mode * * As in orbit mode, when in first-person mode and {@link CameraControl#doublePickFlyTo} is ````true````, we can double-click * or double-tap an {@link Entity} (ie. "double-picking") to fit it in view. This will cause the {@link Camera} to fly to * that Entity. Our target then becomes the center of that Entity. * * Disable that behaviour by setting {@link CameraControl#doublePickFlyTo} ````false````. * * # Plan-View Mode * * In plan-view mode, ````CameraControl```` pans and rotates the {@link Camera}, without rotating it. * * To enable plan-view mode: * * ````javascript * cameraControl.navMode = "planView"; * ```` * * Dolly forwards and backwards by: * * * spinning the mouse wheel, * * pinching on the touch pad, and * * pressing the ````+```` and ````-```` keys. * *
* Pan left, right, up and down by: * * * left-dragging or right-dragging the mouse, and * * tap-dragging the touch pad with SHIFT down. * * Pan forwards, backwards, left, right, up and down by pressing the ````WSADZX```` keys on a QWERTY keyboard, * or ````WSQDWX```` keys on an AZERTY keyboard. *

* * ## Following the Pointer in Plan-View Mode * * When {@link CameraControl#followPointer} is ````true```` in plan-view mode, the mouse or touch pointer will dynamically * indicate the target to which the {@link Camera} will dolly to and from. In plan-view mode, however, the {@link Camera} cannot rotate. * * Lets ensure that we're in plan-view mode, then enable the {@link Camera} to follow the pointer: * * ````javascript * cameraControl.navMode = "planView"; * cameraControl.followPointer = true; // Default * ```` * * When the pointer is over empty space, the target will remain the last object that the pointer was over. * * ## Axis-Aligned Views in Plan-View Mode * * As in orbit and first-person modes, in plan-view mode we can use keys 1-6 to position the {@link Camera} to look at the center of * the {@link Scene} from along each of the six World-space axis. Pressing one of these keys will fly the {@link Camera} to the * corresponding axis-aligned view. * * # CameraControl Events * * ````CameraControl```` fires events as we interact with {@link Entity}s using mouse or touch input. * * The following examples demonstrate how to subscribe to those events. * * The first example shows how to save a handle to a subscription, which we can later use to unsubscribe. * * ## "hover" * * Event fired when the pointer moves while hovering over an Entity. * * ````javascript * const onHover = cameraControl.on("hover", (e) => { * const entity = e.entity; // Entity * const canvasPos = e.canvasPos; // 2D canvas position * }); * ```` * * To unsubscribe from the event: * * ````javascript * cameraControl.off(onHover); * ```` * * ## "hoverOff" * * Event fired when the pointer moves while hovering over empty space. * * ````javascript * cameraControl.on("hoverOff", (e) => { * const canvasPos = e.canvasPos; * }); * ```` * * ## "hoverEnter" * * Event fired when the pointer moves onto an Entity. * * ````javascript * cameraControl.on("hoverEnter", (e) => { * const entity = e.entity; * const canvasPos = e.canvasPos; * }); * ```` * * ## "hoverOut" * * Event fired when the pointer moves off an Entity. * * ````javascript * cameraControl.on("hoverOut", (e) => { * const entity = e.entity; * const canvasPos = e.canvasPos; * }); * ```` * * ## "picked" * * Event fired when we left-click or tap on an Entity. * * ````javascript * cameraControl.on("picked", (e) => { * const entity = e.entity; * const canvasPos = e.canvasPos; * }); * ```` * * ## "pickedSurface" * * Event fired when we left-click or tap on the surface of an Entity. * * ````javascript * cameraControl.on("picked", (e) => { * const entity = e.entity; * const canvasPos = e.canvasPos; * const worldPos = e.worldPos; // 3D World-space position * const viewPos = e.viewPos; // 3D View-space position * const worldNormal = e.worldNormal; // 3D World-space normal vector * }); * ```` * * ## "pickedNothing" * * Event fired when we left-click or tap on empty space. * * ````javascript * cameraControl.on("pickedNothing", (e) => { * const canvasPos = e.canvasPos; * }); * ```` * * ## "doublePicked" * * Event fired wwhen we left-double-click or double-tap on an Entity. * * ````javascript * cameraControl.on("doublePicked", (e) => { * const entity = e.entity; * const canvasPos = e.canvasPos; * }); * ```` * * ## "doublePickedSurface" * * Event fired when we left-double-click or double-tap on the surface of an Entity. * * ````javascript * cameraControl.on("doublePickedSurface", (e) => { * const entity = e.entity; * const canvasPos = e.canvasPos; * const worldPos = e.worldPos; * const viewPos = e.viewPos; * const worldNormal = e.worldNormal; * }); * ```` * * ## "doublePickedNothing" * * Event fired when we left-double-click or double-tap on empty space. * * ````javascript * cameraControl.on("doublePickedNothing", (e) => { * const canvasPos = e.canvasPos; * }); * ```` * * ## "rightClick" * * Event fired when we right-click on the canvas. * * ````javascript * cameraControl.on("rightClick", (e) => { * const event = e.event; // Mouse event * const canvasPos = e.canvasPos; * }); * ```` * * ## Custom Keyboard Mappings * * We can customize````CameraControl```` key bindings as shown below. * * In this example, we'll just set the default bindings for a QWERTY keyboard. * * ````javascript * const input = myViewer.scene.input; * * cameraControl.navMode = "orbit"; * cameraControl.followPointer = true; * * const keyMap = {}; * * keyMap[cameraControl.PAN_LEFT] = [input.KEY_A]; * keyMap[cameraControl.PAN_RIGHT] = [input.KEY_D]; * keyMap[cameraControl.PAN_UP] = [input.KEY_Z]; * keyMap[cameraControl.PAN_DOWN] = [input.KEY_X]; * keyMap[cameraControl.DOLLY_FORWARDS] = [input.KEY_W, input.KEY_ADD]; * keyMap[cameraControl.DOLLY_BACKWARDS] = [input.KEY_S, input.KEY_SUBTRACT]; * keyMap[cameraControl.ROTATE_X_POS] = [input.KEY_DOWN_ARROW]; * keyMap[cameraControl.ROTATE_X_NEG] = [input.KEY_UP_ARROW]; * keyMap[cameraControl.ROTATE_Y_POS] = [input.KEY_LEFT_ARROW]; * keyMap[cameraControl.ROTATE_Y_NEG] = [input.KEY_RIGHT_ARROW]; * keyMap[cameraControl.AXIS_VIEW_RIGHT] = [input.KEY_NUM_1]; * keyMap[cameraControl.AXIS_VIEW_BACK] = [input.KEY_NUM_2]; * keyMap[cameraControl.AXIS_VIEW_LEFT] = [input.KEY_NUM_3]; * keyMap[cameraControl.AXIS_VIEW_FRONT] = [input.KEY_NUM_4]; * keyMap[cameraControl.AXIS_VIEW_TOP] = [input.KEY_NUM_5]; * keyMap[cameraControl.AXIS_VIEW_BOTTOM] = [input.KEY_NUM_6]; * keyMap[cameraControl.MOUSE_PAN] = [[input.KEY_SHIFT, input.MOUSE_LEFT_BUTTON]]; * keyMap[cameraControl.MOUSE_ROTATE] = [ * [input.KEY_SHIFT, input.MOUSE_MIDDLE_BUTTON], * [input.KEY_SHIFT, input.MOUSE_RIGHT_BUTTON] * ] * keyMap[cameraControl.MOUSE_DOLLY] = [[input.KEY_CTRL, input.MOUSE_RIGHT_BUTTON]]; * * cameraControl.keyMap = keyMap; * ```` * * We can also just configure default bindings for a specified keyboard layout, like this: * * ````javascript * cameraControl.keyMap = "qwerty"; * ```` * * Then, ````CameraControl```` will internally set {@link CameraControl#keyMap} to the default key map for the QWERTY * layout (which is the same set of mappings we set in the previous example). In other words, if we subsequently * read {@link CameraControl#keyMap}, it will now be a key map, instead of the "qwerty" string value we set it to. * * Supported layouts are, so far: * * * ````"qwerty"```` * * ````"azerty"```` * * ## Basic Keyboard Mapping * * ````"OR" Relation```` * Set multiple keys to trigger an action if any one is pressed: * * ````javascript * keyMap[cameraControl.DOLLY_BACKWARDS] = [input.KEY_S, input.KEY_SUBTRACT]; * ```` * * If either ````KEY_S```` or ````KEY_SUBTRACT```` is pressed, the camera will dolly backward. * * * ````"AND" Relation```` * To require all keys in a combination to be pressed: * * ````javascript * keyMap[cameraControl.DOLLY_BACKWARDS] = [[input.KEY_S, input.KEY_SUBTRACT]]; * ```` * * The camera will dolly backward if ````both KEY_S```` and ````KEY_SUBTRACT```` are pressed. * * * ````Mix "AND" and "OR" Relation```` * Use a combination of keys and groups for flexibility: * * ````javascript * keyMap[cameraControl.DOLLY_BACKWARDS] = [ * [input.KEY_S, input.KEY_SUBTRACT], // 'And' group * input.KEY_SHIFT // 'Or' with previous * ]; * ```` * * The camera will dolly backward if ````KEY_S```` + ````KEY_SUBTRACT```` are pressed together, or if ````KEY_SHIFT```` is pressed. * * ## Special Mouse Actions * Certain actions support combinations with specific mouse buttons or events: * * * ````MOUSE_PAN```` * For panning with a key and mouse movement: * * ````javascript * keyMap[cameraControl.MOUSE_PAN] = [[input.KEY_SHIFT, input.MOUSE_LEFT_BUTTON]]; * ```` * * Panning is triggered by pressing ````KEY_SHIFT```` + ````MOUSE_LEFT_BUTTON```` while moving the mouse. * * * ````MOUSE_ROTATE```` * Similar to panning, rotation can be configured with key and mouse button combinations: * * ````javascript * keyMap[cameraControl.MOUSE_ROTATE] = [[input.KEY_CTRL, input.MOUSE_LEFT_BUTTON]]; * ```` * * Rotation is triggered by pressing ````KEY_CTRL```` + ````MOUSE_LEFT_BUTTON```` during mouse movement. * * * ````MOUSE_DOLLY```` * Dolly with mouse wheel scrolling and optional key combinations: * * ````javascript * keyMap[cameraControl.MOUSE_DOLLY] = [[input.KEY_ALT]]; * ```` * * Dolly action occurs when scrolling the mouse wheel with ````KEY_ALT```` held (no need to specify MOUSE_WHEEL). * * ## Special Mouse Actions * Use ````input.MOUSE_LEFT_BUTTON````, ````input.MOUSE_MIDDLE_BUTTON````, and ````input.MOUSE_RIGHT_BUTTON```` in combinations only for camera movements involving the mouse. */ class CameraControl extends Component { /** * @private * @constructor */ constructor(owner, cfg = {}) { super(owner, cfg); /** * Identifies the XX action. * @final * @type {Number} */ this.PAN_LEFT = 0; /** * Identifies the XX action. * @final * @type {Number} */ this.PAN_RIGHT = 1; /** * Identifies the XX action. * @final * @type {Number} */ this.PAN_UP = 2; /** * Identifies the XX action. * @final * @type {Number} */ this.PAN_DOWN = 3; /** * Identifies the XX action. * @final * @type {Number} */ this.PAN_FORWARDS = 4; /** * Identifies the XX action. * @final * @type {Number} */ this.PAN_BACKWARDS = 5; /** * Identifies the XX action. * @final * @type {Number} */ this.ROTATE_X_POS = 6; /** * Identifies the XX action. * @final * @type {Number} */ this.ROTATE_X_NEG = 7; /** * Identifies the XX action. * @final * @type {Number} */ this.ROTATE_Y_POS = 8; /** * Identifies the XX action. * @final * @type {Number} */ this.ROTATE_Y_NEG = 9; /** * Identifies the XX action. * @final * @type {Number} */ this.DOLLY_FORWARDS = 10; /** * Identifies the XX action. * @final * @type {Number} */ this.DOLLY_BACKWARDS = 11; /** * Identifies the XX action. * @final * @type {Number} */ this.AXIS_VIEW_RIGHT = 12; /** * Identifies the XX action. * @final * @type {Number} */ this.AXIS_VIEW_BACK = 13; /** * Identifies the XX action. * @final * @type {Number} */ this.AXIS_VIEW_LEFT = 14; /** * Identifies the XX action. * @final * @type {Number} */ this.AXIS_VIEW_FRONT = 15; /** * Identifies the XX action. * @final * @type {Number} */ this.AXIS_VIEW_TOP = 16; /** * Identifies the XX action. * @final * @type {Number} */ this.AXIS_VIEW_BOTTOM = 17; /** * Identifies the XX action. * @final * @type {Number} */ this.MOUSE_PAN = 18; /** * Identifies the XX action. * @final * @type {Number} */ this.MOUSE_ROTATE = 19; /** * Identifies the XX action. * @final * @type {Number} */ this.MOUSE_DOLLY = 20; this._keyMap = {}; // Maps key codes to the above actions this.scene.canvas.canvas.oncontextmenu = (e) => { e.preventDefault(); }; // User-settable CameraControl configurations this._configs = { // Private longTapTimeout: 600, // Millisecs longTapRadius: 5, // Pixels // General active: true, keyboardLayout: "qwerty", navMode: "orbit", planView: false, firstPerson: false, followPointer: true, doublePickFlyTo: true, panRightClick: true, showPivot: false, pointerEnabled: true, constrainVertical: false, smartPivot: false, doubleClickTimeFrame: 250, zoomOnMouseWheel: true, snapToVertex: DEFAULT_SNAP_VERTEX, snapToEdge: DEFAULT_SNAP_EDGE, snapRadius: DEFAULT_SNAP_PICK_RADIUS, keyboardEnabledOnlyIfMouseover: true, // Rotation dragRotationRate: 360.0, keyboardRotationRate: 90.0, rotationInertia: 0.0, // Panning keyboardPanRate: 1.0, touchPanRate: 1.0, panInertia: 0.5, // Dollying keyboardDollyRate: 10, mouseWheelDollyRate: 100, touchDollyRate: 0.2, dollyInertia: 0, dollyProximityThreshold: 30.0, dollyMinSpeed: 0.04 }; // Current runtime state of the CameraControl this._states = { pointerCanvasPos: math.vec2(), mouseover: false, followPointerDirty: true, mouseDownClientX: 0, mouseDownClientY: 0, mouseDownCursorX: 0, mouseDownCursorY: 0, touchStartTime: null, activeTouches: [], tapStartPos: math.vec2(), tapStartTime: -1, lastTapTime: -1, longTouchTimeout: null }; // Updates for CameraUpdater to process on next Scene "tick" event this._updates = { rotateDeltaX: 0, rotateDeltaY: 0, panDeltaX: 0, panDeltaY: 0, panDeltaZ: 0, dollyDelta: 0 }; // Controllers to assist input event handlers with controlling the Camera const scene = this.scene; this._controllers = { cameraControl: this, pickController: new PickController(this, this._configs), pivotController: new PivotController(scene, this._configs), panController: new PanController(scene), cameraFlight: new CameraFlightAnimation(this, { duration: 0.5 }) }; // Input event handlers this._handlers = [ new MouseMiscHandler(this.scene, this._controllers, this._configs, this._states, this._updates), new TouchPanRotateAndDollyHandler(this.scene, this._controllers, this._configs, this._states, this._updates), new MousePanRotateDollyHandler(this.scene, this._controllers, this._configs, this._states, this._updates), new KeyboardAxisViewHandler(this.scene, this._controllers, this._configs, this._states, this._updates), new MousePickHandler(this.scene, this._controllers, this._configs, this._states, this._updates), new TouchPickHandler(this.scene, this._controllers, this._configs, this._states, this._updates), new KeyboardPanRotateDollyHandler(this.scene, this._controllers, this._configs, this._states, this._updates) ]; this._cursors = { dollyForward: "zoom-in", dollyBackward: "zoom-out", rotate: 'grabbing', pan: 'move', }; // Applies scheduled updates to the Camera on each Scene "tick" event this._cameraUpdater = new CameraUpdater(this.scene, this._controllers, this._configs, this._states, this._updates); // Set initial user configurations this.navMode = cfg.navMode; if (cfg.planView) { this.planView = cfg.planView; } this.constrainVertical = cfg.constrainVertical; if (cfg.keyboardLayout) { this.keyboardLayout = cfg.keyboardLayout; // Deprecated } else { this.keyMap = cfg.keyMap; } this.doublePickFlyTo = cfg.doublePickFlyTo; this.panRightClick = cfg.panRightClick; this.active = cfg.active; this.followPointer = cfg.followPointer; this.rotationInertia = cfg.rotationInertia; this.keyboardPanRate = cfg.keyboardPanRate; this.touchPanRate = cfg.touchPanRate; this.keyboardRotationRate = cfg.keyboardRotationRate; this.dragRotationRate = cfg.dragRotationRate; this.touchDollyRate = cfg.touchDollyRate; this.dollyInertia = cfg.dollyInertia; this.dollyProximityThreshold = cfg.dollyProximityThreshold; this.dollyMinSpeed = cfg.dollyMinSpeed; this.panInertia = cfg.panInertia; this.pointerEnabled = true; this.keyboardDollyRate = cfg.keyboardDollyRate; this.mouseWheelDollyRate = cfg.mouseWheelDollyRate; } /** * Sets custom mappings of keys to ````CameraControl```` actions. * * See class docs for usage. * * @param {{Number:Number}|String} value Either a set of new key mappings, or a string to select a keyboard layout, * which causes ````CameraControl```` to use the default key mappings for that layout. */ set keyMap(value) { value = value || "qwerty"; if (utils.isString(value)) { const input = this.scene.input; const keyMap = {}; switch (value) { default: this.error("Unsupported value for 'keyMap': " + value + " defaulting to 'qwerty'"); // Intentional fall-through to "qwerty" case "qwerty": keyMap[this.PAN_LEFT] = [input.KEY_A]; keyMap[this.PAN_RIGHT] = [input.KEY_D]; keyMap[this.PAN_UP] = [input.KEY_Z]; keyMap[this.PAN_DOWN] = [input.KEY_X]; keyMap[this.PAN_BACKWARDS] = []; keyMap[this.PAN_FORWARDS] = []; keyMap[this.DOLLY_FORWARDS] = [input.KEY_W, input.KEY_ADD]; keyMap[this.DOLLY_BACKWARDS] = [input.KEY_S, input.KEY_SUBTRACT]; keyMap[this.ROTATE_X_POS] = [input.KEY_DOWN_ARROW]; keyMap[this.ROTATE_X_NEG] = [input.KEY_UP_ARROW]; keyMap[this.ROTATE_Y_POS] = [input.KEY_Q, input.KEY_LEFT_ARROW]; keyMap[this.ROTATE_Y_NEG] = [input.KEY_E, input.KEY_RIGHT_ARROW]; keyMap[this.AXIS_VIEW_RIGHT] = [input.KEY_NUM_1]; keyMap[this.AXIS_VIEW_BACK] = [input.KEY_NUM_2]; keyMap[this.AXIS_VIEW_LEFT] = [input.KEY_NUM_3]; keyMap[this.AXIS_VIEW_FRONT] = [input.KEY_NUM_4]; keyMap[this.AXIS_VIEW_TOP] = [input.KEY_NUM_5]; keyMap[this.AXIS_VIEW_BOTTOM] = [input.KEY_NUM_6]; keyMap[this.MOUSE_PAN] = [ [input.MOUSE_LEFT_BUTTON, input.KEY_SHIFT], this._configs.panRightClick ? input.MOUSE_RIGHT_BUTTON : input.MOUSE_MIDDLE_BUTTON ]; keyMap[this.MOUSE_ROTATE] = [input.MOUSE_LEFT_BUTTON]; keyMap[this.MOUSE_DOLLY] = []; break; case "azerty": keyMap[this.PAN_LEFT] = [input.KEY_Q]; keyMap[this.PAN_RIGHT] = [input.KEY_D]; keyMap[this.PAN_UP] = [input.KEY_W]; keyMap[this.PAN_DOWN] = [input.KEY_X]; keyMap[this.PAN_BACKWARDS] = []; keyMap[this.PAN_FORWARDS] = []; keyMap[this.DOLLY_FORWARDS] = [input.KEY_Z, input.KEY_ADD]; keyMap[this.DOLLY_BACKWARDS] = [input.KEY_S, input.KEY_SUBTRACT]; keyMap[this.ROTATE_X_POS] = [input.KEY_DOWN_ARROW]; keyMap[this.ROTATE_X_NEG] = [input.KEY_UP_ARROW]; keyMap[this.ROTATE_Y_POS] = [input.KEY_A, input.KEY_LEFT_ARROW]; keyMap[this.ROTATE_Y_NEG] = [input.KEY_E, input.KEY_RIGHT_ARROW]; keyMap[this.AXIS_VIEW_RIGHT] = [input.KEY_NUM_1]; keyMap[this.AXIS_VIEW_BACK] = [input.KEY_NUM_2]; keyMap[this.AXIS_VIEW_LEFT] = [input.KEY_NUM_3]; keyMap[this.AXIS_VIEW_FRONT] = [input.KEY_NUM_4]; keyMap[this.AXIS_VIEW_TOP] = [input.KEY_NUM_5]; keyMap[this.AXIS_VIEW_BOTTOM] = [input.KEY_NUM_6]; keyMap[this.MOUSE_PAN] = [ [input.MOUSE_LEFT_BUTTON, input.KEY_SHIFT], this._configs.panRightClick ? input.MOUSE_RIGHT_BUTTON : input.MOUSE_MIDDLE_BUTTON ]; keyMap[this.MOUSE_ROTATE] = [input.MOUSE_LEFT_BUTTON]; keyMap[this.MOUSE_DOLLY] = []; break; } this._keyMap = keyMap; } else { const keyMap = value; this._keyMap = keyMap; } } /** * Gets custom mappings of keys to {@link CameraControl} actions. * * @returns {{Number:Number}} Current key mappings. */ get keyMap() { return this._keyMap; } _areAllKeysDown(keyDownMap, keys) { if (!keys || keys.length <= 0) { return true; } if (!keyDownMap) { return false; } for (let i = 0, len = keys.length; i < len; i++) { const key = keys[i]; if (!keyDownMap[key]) return false; } return true; } _isAnyOtherKeyDown(keyDownMap, keyMap) { for (let i = 0, len = keyDownMap.length; i < len; i++) { if (keyDownMap[i]) { if (Array.isArray(keyMap)) { if (keyMap.indexOf(i) < 0) return true; } else if (i !== keyMap) return true; } } return false; } _isMouseAction(action) { switch (action) { case this.MOUSE_ROTATE: case this.MOUSE_DOLLY: case this.MOUSE_PAN: return true; default: return false; } } /** * Returns true if any keys configured for the given action are down. * @param action * @param keyDownMap * @private */ _isKeyDownForAction(action, keyDownMap) { const keys = this._keyMap[action]; if (!keys) { return false; } if (keys.length === 0 && this._isMouseAction(action)) return true; if (!keyDownMap) { keyDownMap = this.scene.input.keyDown; } for (let i = 0, len = keys.length; i < len; i++) { const key = keys[i]; if (!Array.isArray(key)) { if (keyDownMap[key] && !this._isAnyOtherKeyDown(keyDownMap, key)) return true; } else { if (this._areAllKeysDown(keyDownMap, key) && !this._isAnyOtherKeyDown(keyDownMap, key)) return true; } } return false; } /** * Sets the HTMl element to represent the pivot point when {@link CameraControl#followPointer} is true. * * See class comments for an example. * * @param {HTMLElement} element HTML element representing the pivot point. */ set pivotElement(element) { this._controllers.pivotController.setPivotElement(element); } /** * Sets if this ````CameraControl```` is active or not. * * When inactive, the ````CameraControl```` will not react to input. * * Default is ````true````. * * @param {Boolean} value Set ````true```` to activate this ````CameraControl````. */ set active(value) { value = value !== false; this._configs.active = value; this._handlers[1]._active = value; this._handlers[5]._active = value; } /** * Gets if this ````CameraControl```` is active or not. * * When inactive, the ````CameraControl```` will not react to input. * * Default is ````true````. * * @returns {Boolean} Returns ````true```` if this ````CameraControl```` is active. */ get active() { return this._configs.active; } /** * Sets whether the pointer snap to vertex. * * @param {boolean} snapToVertex */ set snapToVertex(snapToVertex) { this._configs.snapToVertex = !!snapToVertex; } /** * Gets whether the pointer snap to vertex. * * @returns {boolean} */ get snapToVertex() { return this._configs.snapToVertex; } /** * Sets whether the pointer snap to edge. * * @param {boolean} snapToEdge */ set snapToEdge(snapToEdge) { this._configs.snapToEdge = !!snapToEdge; } /** * Gets whether the pointer snap to edge. * * @returns {boolean} */ get snapToEdge() { return this._configs.snapToEdge; } /** * Sets the current snap radius for "hoverSnapOrSurface" events, to specify whether the radius * within which the pointer snaps to the nearest vertex or the nearest edge. * * Default value is 30 pixels. * * @param {Number} snapRadius The snap radius. */ set snapRadius(snapRadius) { snapRadius = snapRadius || DEFAULT_SNAP_PICK_RADIUS; this._configs.snapRadius = snapRadius; } /** * Gets the current snap radius. * * @returns {Number} The snap radius. */ get snapRadius() { return this._configs.snapRadius; } /** * If `true`, the keyboard shortcuts are enabled ONLY if the mouse is over the canvas. * * @param {boolean} value */ set keyboardEnabledOnlyIfMouseover(value) { this._configs.keyboardEnabledOnlyIfMouseover = !!value; } /** * Gets whether the keyboard shortcuts are enabled ONLY if the mouse is over the canvas or ALWAYS. * * @returns {boolean} */ get keyboardEnabledOnlyIfMouseover() { return this._configs.keyboardEnabledOnlyIfMouseover; } /** * Sets the current navigation mode. * * Accepted values are: * * * "orbit" - rotation orbits about the current target or pivot point, * * "firstPerson" - rotation is about the current eye position, * * "planView" - rotation is disabled. * * See class comments for more info. * * @param {String} navMode The navigation mode: "orbit", "firstPerson" or "planView". */ set navMode(navMode) { navMode = navMode || "orbit"; if (navMode !== "firstPerson" && navMode !== "orbit" && navMode !== "planView") { this.error("Unsupported value for navMode: " + navMode + " - supported values are 'orbit', 'firstPerson' and 'planView' - defaulting to 'orbit'"); navMode = "orbit"; } this._configs.firstPerson = (navMode === "firstPerson"); this._configs.planView = (navMode === "planView"); if (this._configs.firstPerson || this._configs.planView) { this._controllers.pivotController.hidePivot(); this._controllers.pivotController.endPivot(); } this._configs.navMode = navMode; } /** * Gets the current navigation mode. * * @returns {String} The navigation mode: "orbit", "firstPerson" or "planView". */ get navMode() { return this._configs.navMode; } /** * Sets whether mouse and touch input is enabled. * * Default is ````true````. * * Disabling mouse and touch input on ````CameraControl```` is useful when we want to temporarily use mouse or * touch input to interact with some other 3D control, without disturbing the {@link Camera}. * * @param {Boolean} value Set ````true```` to enable mouse and touch input. */ set pointerEnabled(value) { this._reset(); this._configs.pointerEnabled = !!value; } /** * Sets the cursor to be used when a particular action is being performed. * * Accepted actions are: * * * "dollyForward" - when the camera is dollying in the forward direction * * "dollyBackward" - when the camera is dollying in the backward direction * * "pan" - when the camera is being panned * * "rotate" - when the camera is being rotated * * @param {String} action * @param {String} style */ setCursorStyle(action, style) { if (Object.prototype.hasOwnProperty.call(this._cursors, action)) { this._cursors = { ...this._cursors, [action]: style }; } else console.warn(`Action '${action}' is not valid for cursor styles.`); } /** * Gets the current style for a particular action. * * @param {String} action To get the style for * @returns {String} style set on the cursor for action */ getCursorStyle(action) { return this._cursors[action] || null; } _reset() { for (let i = 0, len = this._handlers.length; i < len; i++) { const handler = this._handlers[i]; if (handler.reset) { handler.reset(); } } this._updates.panDeltaX = 0; this._updates.panDeltaY = 0; this._updates.rotateDeltaX = 0; this._updates.rotateDeltaY = 0; this._updates.dolyDelta = 0; } /** * Gets whether mouse and touch input is enabled. * * Default is ````true````. * * Disabling mouse and touch input on ````CameraControl```` is desirable when we want to temporarily use mouse or * touch input to interact with some other 3D control, without interfering with the {@link Camera}. * * @returns {Boolean} Returns ````true```` if mouse and touch input is enabled. */ get pointerEnabled() { return this._configs.pointerEnabled; } /** * Sets whether the {@link Camera} follows the mouse/touch pointer. * * In orbiting mode, the Camera will orbit about the pointer, and will dolly to and from the pointer. * * In fly-to mode, the Camera will dolly to and from the pointer, however the World will always rotate about the Camera position. * * In plan-view mode, the Camera will dolly to and from the pointer, however the Camera will not rotate. * * Default is ````true````. * * See class comments for more info. * * @param {Boolean} value Set ````true```` to enable the Camera to follow the pointer. */ set followPointer(value) { this._configs.followPointer = (value !== false); } /** * Sets whether the {@link Camera} follows the mouse/touch pointer. * * In orbiting mode, the Camera will orbit about the pointer, and will dolly to and from the pointer. * * In fly-to mode, the Camera will dolly to and from the pointer, however the World will always rotate about the Camera position. * * In plan-view mode, the Camera will dolly to and from the pointer, however the Camera will not rotate. * * Default is ````true````. * * See class comments for more info. * * @returns {Boolean} Returns ````true```` if the Camera follows the pointer. */ get followPointer() { return this._configs.followPointer; } /** * Sets the current World-space 3D target position. * * Only applies when {@link CameraControl#followPointer} is ````true````. * * @param {Number[]} worldPos The new World-space 3D target position. */ set pivotPos(worldPos) { this._controllers.pivotController.setPivotPos(worldPos); } /** * Gets the current World-space 3D pivot position. * * Only applies when {@link CameraControl#followPointer} is ````true````. * * @return {Number[]} worldPos The current World-space 3D pivot position. */ get pivotPos() { return this._controllers.pivotController.getPivotPos(); } /** * @deprecated * @param {Boolean} value Set ````true```` to enable dolly-to-pointer behaviour. */ set dollyToPointer(value) { this.warn("dollyToPointer property is deprecated - replaced with followPointer"); this.followPointer = value; } /** * @deprecated * @returns {Boolean} Returns ````true```` if dolly-to-pointer behaviour is enabled. */ get dollyToPointer() { this.warn("dollyToPointer property is deprecated - replaced with followPointer"); return this.followPointer; } /** * @deprecated * @param {Boolean} value Set ````true```` to enable dolly-to-pointer behaviour. */ set panToPointer(value) { this.warn("panToPointer property is deprecated - replaced with followPointer"); } /** * @deprecated * @returns {Boolean} Returns ````true```` if dolly-to-pointer behaviour is enabled. */ get panToPointer() { this.warn("panToPointer property is deprecated - replaced with followPointer"); return false; } /** * Sets whether this ````CameraControl```` is in plan-view mode. * * When in plan-view mode, rotation is disabled. * * Default is ````false````. * * Deprecated - use {@link CameraControl#navMode} instead. * * @param {Boolean} value Set ````true```` to enable plan-view mode. * @deprecated */ set planView(value) { this._configs.planView = !!value; this._configs.firstPerson = false; if (this._configs.planView) { this._controllers.pivotController.hidePivot(); this._controllers.pivotController.endPivot(); } this.warn("planView property is deprecated - replaced with navMode"); } /** * Gets whether this ````CameraControl```` is in plan-view mode. * * When in plan-view mode, rotation is disabled. * * Default is ````false````. * * Deprecated - use {@link CameraControl#navMode} instead. * * @returns {Boolean} Returns ````true```` if plan-view mode is enabled. * @deprecated */ get planView() { this.warn("planView property is deprecated - replaced with navMode"); return this._configs.planView; } /** * Sets whether this ````CameraControl```` is in first-person mode. * * In "first person" mode (disabled by default) the look position rotates about the eye position. Otherwise, {@link Camera#eye} rotates about {@link Camera#look}. * * Default is ````false````. * * Deprecated - use {@link CameraControl#navMode} instead. * * @param {Boolean} value Set ````true```` to enable first-person mode. * @deprecated */ set firstPerson(value) { this.warn("firstPerson property is deprecated - replaced with navMode"); this._configs.firstPerson = !!value; this._configs.planView = false; if (this._configs.firstPerson) { this._controllers.pivotController.hidePivot(); this._controllers.pivotController.endPivot(); } } /** * Gets whether this ````CameraControl```` is in first-person mode. * * In "first person" mode (disabled by default) the look position rotates about the eye position. Otherwise, {@link Camera#eye} rotates about {@link Camera#look}. * * Default is ````false````. * * Deprecated - use {@link CameraControl#navMode} instead. * * @returns {Boolean} Returns ````true```` if first-person mode is enabled. * @deprecated */ get firstPerson() { this.warn("firstPerson property is deprecated - replaced with navMode"); return this._configs.firstPerson; } /** * Sets whether to vertically constrain the {@link Camera} position for first-person navigation. * * When set ````true````, this constrains {@link Camera#eye} to its current vertical position. * * Only applies when {@link CameraControl#navMode} is ````"firstPerson"````. * * Default is ````false````. * * @param {Boolean} value Set ````true```` to vertically constrain the Camera. */ set constrainVertical(value) { this._configs.constrainVertical = !!value; } /** * Gets whether to vertically constrain the {@link Camera} position for first-person navigation. * * When set ````true````, this constrains {@link Camera#eye} to its current vertical position. * * Only applies when {@link CameraControl#navMode} is ````"firstPerson"````. * * Default is ````false````. * * @returns {Boolean} ````true```` when Camera is vertically constrained. */ get constrainVertical() { return this._configs.constrainVertical; } /** * Sets whether double-picking an {@link Entity} causes the {@link Camera} to fly to its boundary. * * Default is ````false````. * * @param {Boolean} value Set ````true```` to enable double-pick-fly-to mode. */ set doublePickFlyTo(value) { this._configs.doublePickFlyTo = value !== false; } /** * Gets whether double-picking an {@link Entity} causes the {@link Camera} to fly to its boundary. * * Default is ````false````. * * @returns {Boolean} Returns ````true```` when double-pick-fly-to mode is enabled. */ get doublePickFlyTo() { return this._configs.doublePickFlyTo; } /** * Sets whether either right-clicking (true) or middle-clicking (false) pans the {@link Camera}. * * Default is ````true````. * * @param {Boolean} value Set ````false```` to disable pan on right-click. */ set panRightClick(value) { this._configs.panRightClick = value !== false; const panKeyMap = this._keyMap[this.MOUSE_PAN]; if (panKeyMap && panKeyMap.length > 0) { const input = this.scene.input; for (let i = 0, len = panKeyMap.length; i < len; i++) { if (this._configs.panRightClick && panKeyMap[i] === input.MOUSE_MIDDLE_BUTTON) this._keyMap[this.MOUSE_PAN][i] = input.MOUSE_RIGHT_BUTTON; else if (!this._configs.panRightClick && panKeyMap[i] === input.MOUSE_RIGHT_BUTTON) this._keyMap[this.MOUSE_PAN][i] = input.MOUSE_MIDDLE_BUTTON; } } } /** * Gets whether right-clicking pans the {@link Camera}. * * Default is ````true````. * * @returns {Boolean} Returns ````false```` when pan on right-click is disabled. */ get panRightClick() { return this._configs.panRightClick; } /** * Sets a factor in range ````[0..1]```` indicating how much the {@link Camera} keeps moving after you finish rotating it. * * A value of ````0.0```` causes it to immediately stop, ````0.5```` causes its movement to decay 50% on each tick, * while ````1.0```` causes no decay, allowing it continue moving, by the current rate of rotation. * * You may choose an inertia of zero when you want be able to precisely rotate the Camera, * without interference from inertia. Zero inertia can also mean that less frames are rendered while * you are rotating the Camera. * * Default is ````0.0````. * * Does not apply when {@link CameraControl#navMode} is ````"planView"````, which disallows rotation. * * @param {Number} rotationInertia New inertial factor. */ set rotationInertia(rotationInertia) { this._configs.rotationInertia = (rotationInertia !== undefined && rotationInertia !== null) ? rotationInertia : 0.0; } /** * Gets the rotation inertia factor. * * Default is ````0.0````. * * Does not apply when {@link CameraControl#navMode} is ````"planView"````, which disallows rotation. * * @returns {Number} The inertia factor. */ get rotationInertia() { return this._configs.rotationInertia; } /** * Sets how much the {@link Camera} pans each second with keyboard input. * * Default is ````5.0````, to pan the Camera ````5.0```` World-space units every second that * a panning key is depressed. See the ````CameraControl```` class documentation for which keys control * panning. * * Panning direction is aligned to our Camera's orientation. When we pan horizontally, we pan * to our left and right, when we pan vertically, we pan upwards and downwards, and when we pan forwards * and backwards, we pan along the direction the Camera is pointing. * * Unlike dollying when {@link followPointer} is ````true````, panning does not follow the pointer. * * @param {Number} keyboardPanRate The new keyboard pan rate. */ set keyboardPanRate(keyboardPanRate) { this._configs.keyboardPanRate = (keyboardPanRate !== null && keyboardPanRate !== undefined) ? keyboardPanRate : 5.0; } /** * Sets how fast the camera pans on touch panning * * @param {Number} touchPanRate The new touch pan rate. */ set touchPanRate(touchPanRate) { this._configs.touchPanRate = (touchPanRate !== null && touchPanRate !== undefined) ? touchPanRate : 1.0; } /** * Gets how fast the {@link Camera} pans on touch panning * * Default is ````1.0````. * * @returns {Number} The current touch pan rate. */ get touchPanRate() { return this._configs.touchPanRate; } /** * Gets how much the {@link Camera} pans each second with keyboard input. * * Default is ````5.0````. * * @returns {Number} The current keyboard pan rate. */ get keyboardPanRate() { return this._configs.keyboardPanRate; } /** * Sets how many degrees per second the {@link Camera} rotates/orbits with keyboard input. * * Default is ````90.0````, to rotate/orbit the Camera ````90.0```` degrees every second that * a rotation key is depressed. See the ````CameraControl```` class documentation for which keys control * rotation/orbit. * * @param {Number} keyboardRotationRate The new keyboard rotation rate. */ set keyboardRotationRate(keyboardRotationRate) { this._configs.keyboardRotationRate = (keyboardRotationRate !== null && keyboardRotationRate !== undefined) ? keyboardRotationRate : 90.0; } /** * Sets how many degrees per second the {@link Camera} rotates/orbits with keyboard input. * * Default is ````90.0````. * * @returns {Number} The current keyboard rotation rate. */ get keyboardRotationRate() { return this._configs.keyboardRotationRate; } /** * Sets the current drag rotation rate. * * This configures how many degrees the {@link Camera} rotates/orbits for a full sweep of the canvas by mouse or touch dragging. * * For example, a value of ````360.0```` indicates that the ````Camera```` rotates/orbits ````360.0```` degrees horizontally * when we sweep the entire width of the canvas. * * ````CameraControl```` makes vertical rotation half as sensitive as horizontal rotation, so that we don't tend to * flip upside-down. Therefore, a value of ````360.0```` rotates/orbits the ````Camera```` through ````180.0```` degrees * vertically when we sweep the entire height of the canvas. * * Default is ````360.0````. * * @param {Number} dragRotationRate The new drag rotation rate. */ set dragRotationRate(dragRotationRate) { this._configs.dragRotationRate = (dragRotationRate !== null && dragRotationRate !== undefined) ? dragRotationRate : 360.0; } /** * Gets the current drag rotation rate. * * Default is ````360.0````. * * @returns {Number} The current drag rotation rate. */ get dragRotationRate() { return this._configs.dragRotationRate; } /** * Sets how much the {@link Camera} dollys each second with keyboard input. * * Default is ````15.0````, to dolly the {@link Camera} ````15.0```` World-space units per second while we hold down * the ````+```` and ````-```` keys. * * @param {Number} keyboardDollyRate The new keyboard dolly rate. */ set keyboardDollyRate(keyboardDollyRate) { this._configs.keyboardDollyRate = (keyboardDollyRate !== null && keyboardDollyRate !== undefined) ? keyboardDollyRate : 15.0; } /** * Gets how much the {@link Camera} dollys each second with keyboard input. * * Default is ````15.0````. * * @returns {Number} The current keyboard dolly rate. */ get keyboardDollyRate() { return this._configs.keyboardDollyRate; } /** * Sets how much the {@link Camera} dollys with touch input. * * Default is ````0.2```` * * @param {Number} touchDollyRate The new touch dolly rate. */ set touchDollyRate(touchDollyRate) { this._configs.touchDollyRate = (touchDollyRate !== null && touchDollyRate !== undefined) ? touchDollyRate : 0.2; } /** * Gets how much the {@link Camera} dollys each second with touch input. * * Default is ````0.2````. * * @returns {Number} The current touch dolly rate. */ get touchDollyRate() { return this._configs.touchDollyRate; } /** * Sets how much the {@link Camera} dollys each second while the mouse wheel is spinning. * * Default is ````100.0````, to dolly the {@link Camera} ````10.0```` World-space units per second as we spin * the mouse wheel. * * @param {Number} mouseWheelDollyRate The new mouse wheel dolly rate. */ set mouseWheelDollyRate(mouseWheelDollyRate) { this._configs.mouseWheelDollyRate = (mouseWheelDollyRate !== null && mouseWheelDollyRate !== undefined) ? mouseWheelDollyRate : 100.0; } /** * Gets how much the {@link Camera} dollys each second while the mouse wheel is spinning. * * Default is ````100.0````. * * @returns {Number} The current mouseWheel dolly rate. */ get mouseWheelDollyRate() { return this._configs.mouseWheelDollyRate; } /** * Sets the dolly inertia factor. * * This factor configures how much the {@link Camera} keeps moving after you finish dollying it. * * This factor is a value in range ````[0..1]````. A value of ````0.0```` causes dollying to immediately stop, * ````0.5```` causes dollying to decay 50% on each animation frame, while ````1.0```` causes no decay, which allows dollying * to continue until further input stops it. * * You might set ````dollyInertia```` to zero when you want be able to precisely position or rotate the Camera, * without interference from inertia. This also means that xeokit renders less frames while dollying the Camera, * which can improve rendering performance. * * Default is ````0````. * * @param {Number} dollyInertia New dolly inertia factor. */ set dollyInertia(dollyInertia) { this._configs.dollyInertia = (dollyInertia !== undefined && dollyInertia !== null) ? dollyInertia : 0; } /** * Gets the dolly inertia factor. * * Default is ````0````. * * @returns {Number} The current dolly inertia factor. */ get dollyInertia() { return this._configs.dollyInertia; } /** * Sets the proximity to the closest object below which dolly speed decreases, and above which dolly speed increases. * * Default is ````35.0````. * * @param {Number} dollyProximityThreshold New dolly proximity threshold. */ set dollyProximityThreshold(dollyProximityThreshold) { this._configs.dollyProximityThreshold = (dollyProximityThreshold !== undefined && dollyProximityThreshold !== null) ? dollyProximityThreshold : 35.0; } /** * Gets the proximity to the closest object below which dolly speed decreases, and above which dolly speed increases. * * Default is ````35.0````. * * @returns {Number} The current dolly proximity threshold. */ get dollyProximityThreshold() { return this._configs.dollyProximityThreshold; } /** * Sets the minimum dolly speed. * * Default is ````0.04````. * * @param {Number} dollyMinSpeed New dolly minimum speed. */ set dollyMinSpeed(dollyMinSpeed) { this._configs.dollyMinSpeed = (dollyMinSpeed !== undefined && dollyMinSpeed !== null) ? dollyMinSpeed : 0.04; } /** * Gets the minimum dolly speed. * * Default is ````0.04````. * * @returns {Number} The current minimum dolly speed. */ get dollyMinSpeed() { return this._configs.dollyMinSpeed; } /** * Sets the pan inertia factor. * * This factor configures how much the {@link Camera} keeps moving after you finish panning it. * * This factor is a value in range ````[0..1]````. A value of ````0.0```` causes panning to immediately stop, * ````0.5```` causes panning to decay 50% on each animation frame, while ````1.0```` causes no decay, which allows panning * to continue until further input stops it. * * You might set ````panInertia```` to zero when you want be able to precisely position or rotate the Camera, * without interference from inertia. This also means that xeokit renders less frames while panning the Camera, * wich can improve rendering performance. * * Default is ````0.5````. * * @param {Number} panInertia New pan inertia factor. */ set panInertia(panInertia) { this._configs.panInertia = (panInertia !== undefined && panInertia !== null) ? panInertia : 0.5; } /** * Gets the pan inertia factor. * * Default is ````0.5````. * * @returns {Number} The current pan inertia factor. */ get panInertia() { return this._configs.panInertia; } /** * Sets the keyboard layout. * * Supported layouts are: * * * ````"qwerty"```` (default) * * ````"azerty"```` * * @deprecated * @param {String} value Selects the keyboard layout. */ set keyboardLayout(value) { // this.warn("keyboardLayout property is deprecated - use keyMap property instead"); value = value || "qwerty"; if (value !== "qwerty" && value !== "azerty") { this.error("Unsupported value for keyboardLayout - defaulting to 'qwerty'"); value = "qwerty"; } this._configs.keyboardLayout = value; this.keyMap = this._configs.keyboardLayout; } /** * Gets the keyboard layout. * * Supported layouts are: * * * ````"qwerty"```` (default) * * ````"azerty"```` * * @deprecated * @returns {String} The current keyboard layout. */ get keyboardLayout() { return this._configs.keyboardLayout; } /** * Sets a sphere as the representation of the pivot position. * * @param {Object} [cfg] Sphere configuration. * @param {String} [cfg.size=1] Optional size factor of the sphere. Defaults to 1. * @param {String} [cfg.material=PhongMaterial] Optional size factor of the sphere. Defaults to a red opaque material. */ enablePivotSphere(cfg = {}) { this._controllers.pivotController.enablePivotSphere(cfg); } /** * Remove the sphere as the representation of the pivot position. * */ disablePivotSphere() { this._controllers.pivotController.disablePivotSphere(); } /** * Sets whether smart default pivoting is enabled. * * When ````true````, we'll pivot by default about the 3D position of the mouse/touch pointer on an * imaginary sphere that's centered at {@link Camera#eye} and sized to the {@link Scene} boundary. * * When ````false````, we'll pivot by default about {@link Camera#look}. * * Default is ````false````. * * @param {Boolean} enabled Set ````true```` to pivot by default about the selected point on the virtual sphere, or ````false```` to pivot by default about {@link Camera#look}. */ set smartPivot(enabled) { this._configs.smartPivot = (enabled !== false); } /** * Gets whether smart default pivoting is enabled. * * When ````true````, we'll pivot by default about the 3D position of the mouse/touch pointer on an * imaginary sphere that's centered at {@link Camera#eye} and sized to the {@link Scene} boundary. * * When ````false````, we'll pivot by default about {@link Camera#look}. * * Default is ````false````. * * @returns {Boolean} Returns ````true```` when pivoting by default about the selected point on the virtual sphere, or ````false```` when pivoting by default about {@link Camera#look}. */ get smartPivot() { return this._configs.smartPivot; } /** * Sets the double click time frame length in milliseconds. * * If two mouse click events occur within this time frame, it is considered a double click. * * Default is ````250```` * * @param {Number} value New double click time frame. */ set doubleClickTimeFrame(value) { this._configs.doubleClickTimeFrame = (value !== undefined && value !== null) ? value : 250; } /** * Gets the double click time frame length in milliseconds. * * Default is ````250```` * * @param {Number} value Current double click time frame. */ get doubleClickTimeFrame() { return this._configs.doubleClickTimeFrame; } /** * Sets whether to zoom the camera on mouse wheel * * Default is ````true```` * * @param {Boolean} enabled */ set zoomOnMouseWheel(enabled) { this._configs.zoomOnMouseWheel = !!enabled; } /** * Gets whether to zoom the camera on mouse wheel * * @returns {Boolean} */ get zoomOnMouseWheel() { return this._configs.zoomOnMouseWheel; } /** * Destroys this ````CameraControl````. * @private */ destroy() { this._destroyHandlers(); this._destroyControllers(); this._cameraUpdater.destroy(); super.destroy(); } _destroyHandlers() { for (let i = 0, len = this._handlers.length; i < len; i++) { const handler = this._handlers[i]; if (handler.destroy) { handler.destroy(); } } } _destroyControllers() { for (let i = 0, len = this._controllers.length; i < len; i++) { const controller = this._controllers[i]; if (controller.destroy) { controller.destroy(); } } } } /** * @desc A property within a {@link PropertySet}. * * @class Property */ class Property { /** * @private */ constructor(name, value, type, valueType, description) { /** * The name of this property. * * @property name * @type {String} */ this.name = name; /** * The type of this property. * * @property type * @type {Number|String} */ this.type = type; /** * The value of this property. * * @property value * @type {*} */ this.value = value; /** * The type of this property's value. * * @property valueType * @type {Number|String} */ this.valueType = valueType; /** * Informative text to explain the property. * * @property name * @type {String} */ this.description = description; } } /** * @desc A set of properties associated with one or more {@link MetaObject}s. * * A PropertySet is created within {@link MetaScene#createMetaModel} and belongs to a {@link MetaModel}. * * Each PropertySet is registered by {@link PropertySet#id} in {@link MetaScene#propertySets} and {@link MetaModel#propertySets}. * * @class PropertySet */ class PropertySet { /** * @private */ constructor(params) { /** * Globally-unique ID for this PropertySet. * * PropertySet instances are registered by this ID in {@link MetaScene#propertySets} and {@link MetaModel#propertySets}. * * @property id * @type {String} */ this.id = params.id; /** * ID of the corresponding object within the originating system, if any. * * @type {String} * @abstract */ this.originalSystemId = params.originalSystemId; /** * The MetaModels that share this PropertySet. * @type {MetaModel[]} */ this.metaModels = []; /** * Human-readable name of this PropertySet. * * @property name * @type {String} */ this.name = params.name; /** * Type of this PropertySet. * * @property type * @type {String} */ this.type = params.type; /** * Properties within this PropertySet. * * @property properties * @type {Property[]} */ this.properties = []; if (params.properties) { const properties = params.properties; for (let i = 0, len = properties.length; i < len; i++) { const property = properties[i]; if (Number.isInteger(property)) { // Will decompress in MetaModel.finalize(); this.properties.push(property); } else { this.properties.push(new Property(property.name, property.value, property.type, property.valueType, property.description)); } } } } } /** * @desc Metadata corresponding to an {@link Entity} that represents an object. * * An {@link Entity} represents an object when {@link Entity#isObject} is ````true```` * * A MetaObject corresponds to an {@link Entity} by having the same {@link MetaObject#id} as the {@link Entity#id}. * * A MetaObject is created within {@link MetaScene#createMetaModel} and belongs to a {@link MetaModel}. * * Each MetaObject is registered by {@link MetaObject#id} in {@link MetaScene#metaObjects}. * * A {@link MetaModel} represents its object structure with a tree of MetaObjects, with {@link MetaModel#rootMetaObject} referencing * the root MetaObject. * * @class MetaObject */ class MetaObject { /** * @private */ constructor(params) { /** * The MetaModels that share this MetaObject. * @type {MetaModel[]} */ this.metaModels = []; /** * Globally-unique ID. * * MetaObject instances are registered by this ID in {@link MetaScene#metaObjects}. * * @property id * @type {String|Number} */ this.id = params.id; /** * ID of the parent MetaObject. * @type {String|Number} */ this.parentId = params.parentId; /** * The parent MetaObject. * @type {MetaObject | null} */ this.parent = null; /** * ID of the corresponding object within the originating system, if any. * * @type {String} * @abstract */ this.originalSystemId = params.originalSystemId; /** * Human-readable name. * * @property name * @type {String} */ this.name = params.name; /** * Type - often an IFC product type. * * @property type * @type {String} */ this.type = params.type; /** * IDs of PropertySets associated with this MetaObject. * @type {[]|*} */ this.propertySetIds = params.propertySetIds; /** * The {@link PropertySet}s associated with this MetaObject. * * @property propertySets * @type {PropertySet[]} */ this.propertySets = []; /** * The attributes of this MetaObject. * @type {{}} */ this.attributes = params.attributes || {}; if (params.external !== undefined && params.external !== null) { /** * External application-specific metadata * * Undefined when there are is no external application-specific metadata. * * @property external * @type {*} */ this.external = params.external; } } /** * Backwards compatibility with the object belonging to a single MetaModel. * * @property metaModel * @type {MetaModel|null} **/ get metaModel() { if (this.metaModels.length == 1) { return this.metaModels[0]; } return null; } /** * Gets the {@link MetaObject#id}s of the {@link MetaObject}s within the subtree. * * @returns {String[]} Array of {@link MetaObject#id}s. */ getObjectIDsInSubtree() { const objectIds = []; function visit(metaObject) { if (!metaObject) { return; } objectIds.push(metaObject.id); const children = metaObject.children; if (children) { for (var i = 0, len = children.length; i < len; i++) { visit(children[i]); } } } visit(this); return objectIds; } /** * Iterates over the {@link MetaObject}s within the subtree. * * @param {Function} callback Callback fired at each {@link MetaObject}. */ withMetaObjectsInSubtree(callback) { function visit(metaObject) { if (!metaObject) { return; } callback(metaObject); const children = metaObject.children; if (children) { for (var i = 0, len = children.length; i < len; i++) { visit(children[i]); } } } visit(this); } /** * Gets the {@link MetaObject#id}s of the {@link MetaObject}s within the subtree that have the given {@link MetaObject#type}s. * * @param {String[]} types {@link MetaObject#type} values. * @returns {String[]} Array of {@link MetaObject#id}s. */ getObjectIDsInSubtreeByType(types) { const mask = {}; for (var i = 0, len = types.length; i < len; i++) { mask[types[i]] = types[i]; } const objectIds = []; function visit(metaObject) { if (!metaObject) { return; } if (mask[metaObject.type]) { objectIds.push(metaObject.id); } const children = metaObject.children; if (children) { for (var i = 0, len = children.length; i < len; i++) { visit(children[i]); } } } visit(this); return objectIds; } /** * Returns properties of this MeteObject as JSON. * * @returns {{id: (String|Number), type: String, name: String, parent: (String|Number|Undefined)}} */ getJSON() { var json = { id: this.id, type: this.type, name: this.name }; if (this.parent) { json.parent = this.parent.id; } return json; } } /** * @desc Metadata corresponding to an {@link Entity} that represents a model. * * An {@link Entity} represents a model when {@link Entity#isModel} is ````true```` * * A MetaModel corresponds to an {@link Entity} by having the same {@link MetaModel#id} as the {@link Entity#id}. * * A MetaModel is created by {@link MetaScene#createMetaModel} and belongs to a {@link MetaScene}. * * Each MetaModel is registered by {@link MetaModel#id} in {@link MetaScene#metaModels}. * * A {@link MetaModel} represents its object structure with a tree of {@link MetaObject}s, with {@link MetaModel#rootMetaObject} referencing the root {@link MetaObject}. * * @class MetaModel */ class MetaModel { /** * Creates a new, unfinalized MetaModel. * * * The MetaModel is immediately registered by {@link MetaModel#id} in {@link MetaScene#metaModels}, even though it's not yet populated. * * The MetaModel then needs to be populated with one or more calls to {@link metaModel#loadData}. * * As we populate it, the MetaModel will create {@link MetaObject}s and {@link PropertySet}s in itself, and in the MetaScene. * * When populated, call {@link MetaModel#finalize} to finish it off, which causes MetaScene to fire a "metaModelCreated" event. */ constructor(params) { /** * Globally-unique ID. * * MetaModels are registered by ID in {@link MetaScene#metaModels}. * * When this MetaModel corresponds to an {@link Entity} then this ID will match the {@link Entity#id}. * * @property id * @type {String|Number} */ this.id = params.id; /** * The project ID * @property projectId * @type {String|Number} */ this.projectId = params.projectId; /** * The revision ID, if available. * * Will be undefined if not available. * * @property revisionId * @type {String|Number} */ this.revisionId = params.revisionId; /** * The model author, if available. * * Will be undefined if not available. * * @property author * @type {String} */ this.author = params.author; /** * The date the model was created, if available. * * Will be undefined if not available. * * @property createdAt * @type {String} */ this.createdAt = params.createdAt; /** * The application that created the model, if available. * * Will be undefined if not available. * * @property creatingApplication * @type {String} */ this.creatingApplication = params.creatingApplication; /** * The model schema version, if available. * * Will be undefined if not available. * * @property schema * @type {String} */ this.schema = params.schema; /** * Metadata on the {@link Scene}. * * @property metaScene * @type {MetaScene} */ this.metaScene = params.metaScene; /** * The {@link PropertySet}s in this MetaModel. * * @property propertySets * @type {PropertySet[]} */ this.propertySets = []; /** * The root {@link MetaObject}s in this MetaModel's composition structure hierarchy. * * @property rootMetaObject * @type {MetaObject[]} */ this.rootMetaObjects = []; /** * The {@link MetaObject}s in this MetaModel, each mapped to its ID. * * @property metaObjects * @type {{String:MetaObject}} */ this.metaObjects = {}; /** * Connectivity graph. * @type {{}} */ this.graph = params.graph || {}; this.metaScene.metaModels[this.id] = this; this._propertyLookup = []; /** * True when this MetaModel has been finalized. * @type {boolean} */ this.finalized = false; } /** * Backwards compatibility with the model having a single root MetaObject. * * @property rootMetaObject * @type {MetaObject|null} */ get rootMetaObject() { if (this.rootMetaObjects.length === 1) { return this.rootMetaObjects[0]; } return null; } /** * Load metamodel data into this MetaModel. * @param metaModelData */ loadData(metaModelData, options = {}) { if (this.finalized) { throw "MetaScene already finalized - can't add more data"; } this._globalizeIDs(metaModelData, options); const metaScene = this.metaScene; const propertyLookup = metaModelData.properties; if (propertyLookup) { for (let i = 0, len = propertyLookup.length; i < len; i++) { this._propertyLookup.push(propertyLookup[i]); } } // Create global Property Sets if (metaModelData.propertySets) { for (let i = 0, len = metaModelData.propertySets.length; i < len; i++) { const propertySetData = metaModelData.propertySets[i]; if (!propertySetData.properties) { // HACK: https://github.com/Creoox/creoox-ifc2gltfcxconverter/issues/8 propertySetData.properties = []; } let propertySet = metaScene.propertySets[propertySetData.id]; if (!propertySet) { propertySet = new PropertySet({ id: propertySetData.id, originalSystemId: propertySetData.originalSystemId || propertySetData.id, type: propertySetData.type, name: propertySetData.name, properties: propertySetData.properties }); metaScene.propertySets[propertySet.id] = propertySet; } propertySet.metaModels.push(this); this.propertySets.push(propertySet); } } if (metaModelData.metaObjects) { for (let i = 0, len = metaModelData.metaObjects.length; i < len; i++) { const metaObjectData = metaModelData.metaObjects[i]; const id = metaObjectData.id; let metaObject = metaScene.metaObjects[id]; if (!metaObject) { const type = metaObjectData.type; const originalSystemId = metaObjectData.originalSystemId; const propertySetIds = metaObjectData.propertySets || metaObjectData.propertySetIds; metaObject = new MetaObject({ id, originalSystemId, parentId: metaObjectData.parent, type, name: metaObjectData.name, attributes: metaObjectData.attributes, propertySetIds, external: metaObjectData.external, }); this.metaScene.metaObjects[id] = metaObject; metaObject.metaModels = []; } this.metaObjects[id] = metaObject; if (!metaObjectData.parent) { this.rootMetaObjects.push(metaObject); metaScene.rootMetaObjects[id] = metaObject; } metaObject.metaModels.push(this); } } } _decompressProperties(propertyLookup, properties) { const propsNotFound = []; for (let i = 0, len = properties.length; i < len; i++) { const property = properties[i]; if (Number.isInteger(property)) { const lookupProperty = propertyLookup[property]; if (lookupProperty) { properties[i] = lookupProperty; } else { propsNotFound.push(property); } } } if (propsNotFound.length > 0) { console.error(`[MetaModel._decompressProperties] Properties not found: ${propsNotFound}`); } } finalize() { if (this.finalized) { throw "MetaScene already finalized - can't re-finalize"; } // Re-link MetaScene's entire MetaObject parent/child hierarchy const metaScene = this.metaScene; for (let objectId in metaScene.metaObjects) { const metaObject = metaScene.metaObjects[objectId]; if (metaObject.children) { metaObject.children = []; } // Re-link each MetaObject's property sets if (metaObject.propertySets) { metaObject.propertySets = []; } if (metaObject.propertySetIds) { for (let i = 0, len = metaObject.propertySetIds.length; i < len; i++) { const propertySetId = metaObject.propertySetIds[i]; const propertySet = metaScene.propertySets[propertySetId]; metaObject.propertySets.push(propertySet); } } } for (let objectId in metaScene.metaObjects) { const metaObject = metaScene.metaObjects[objectId]; if (metaObject.parentId) { const parentMetaObject = metaScene.metaObjects[metaObject.parentId]; if (parentMetaObject) { metaObject.parent = parentMetaObject; (parentMetaObject.children || (parentMetaObject.children = [])).push(metaObject); } } } // Relink MetaObjects to their MetaModels for (let objectId in metaScene.metaObjects) { const metaObject = metaScene.metaObjects[objectId]; metaObject.metaModels = []; } for (let modelId in metaScene.metaModels) { const metaModel = metaScene.metaModels[modelId]; for (let objectId in metaModel.metaObjects) { const metaObject = metaModel.metaObjects[objectId]; metaObject.metaModels.push(metaModel); } } // Rebuild MetaScene's MetaObjects-by-type lookup metaScene.metaObjectsByType = {}; for (let objectId in metaScene.metaObjects) { const metaObject = metaScene.metaObjects[objectId]; const type = metaObject.type; (metaScene.metaObjectsByType[type] || (metaScene.metaObjectsByType[type] = {}))[objectId] = metaObject; } // Decompress properties if (this.propertySets) { for (let i = 0, len = this.propertySets.length; i < len; i++) { const propertySet = this.propertySets[i]; this._decompressProperties(this._propertyLookup, propertySet.properties); } } this._propertyLookup = []; this.finalized = true; this.metaScene.fire("metaModelCreated", this.id); } /** * Gets this MetaModel as JSON. * @returns {{schema: (String|string|*), createdAt: (String|string|*), metaObjects: *[], author: (String|string|*), id: (String|Number|string|number|*), creatingApplication: (String|string|*), projectId: (String|Number|string|number|*), propertySets: *[]}} */ getJSON() { const json = { id: this.id, projectId: this.projectId, author: this.author, createdAt: this.createdAt, schema: this.schema, creatingApplication: this.creatingApplication, metaObjects: [], propertySets: [] }; const metaObjectsList = Object.values(this.metaObjects); for (let i = 0, len = metaObjectsList.length; i < len; i++) { const metaObject = metaObjectsList[i]; const metaObjectCfg = { id: metaObject.id, originalSystemId: metaObject.originalSystemId, extId: metaObject.extId, type: metaObject.type, name: metaObject.name }; if (metaObject.parent) { metaObjectCfg.parent = metaObject.parent.id; } if (metaObject.attributes) { metaObjectCfg.attributes = metaObject.attributes; } if (metaObject.propertySetIds) { metaObjectCfg.propertySetIds = metaObject.propertySetIds; } json.metaObjects.push(metaObjectCfg); } for (let i = 0, len = this.propertySets.length; i < len; i++) { const propertySet = this.propertySets[i]; const propertySetCfg = { id: propertySet.id, originalSystemId: propertySet.originalSystemId, extId: propertySet.extId, type: propertySet.type, name: propertySet.name, propertyies: [] }; for (let j = 0, lenj = propertySet.properties.length; j < lenj; j++) { const property = propertySet.properties[j]; const propertyCfg = { id: property.id, description: property.description, type: property.type, name: property.name, value: property.value, valueType: property.valueType }; propertySetCfg.properties.push(propertyCfg); } json.propertySets.push(propertySetCfg); } return json; } _globalizeIDs(metaModelData, options) { const globalize = !!options.globalizeObjectIds; if (metaModelData.metaObjects) { for (let i = 0, len = metaModelData.metaObjects.length; i < len; i++) { const metaObjectData = metaModelData.metaObjects[i]; // Globalize MetaObject IDs and parent IDs metaObjectData.originalSystemId = metaObjectData.id; if (metaObjectData.parent) { metaObjectData.originalParentSystemId = metaObjectData.parent; } if (globalize) { metaObjectData.id = math.globalizeObjectId(this.id, metaObjectData.id); if (metaObjectData.parent) { metaObjectData.parent = math.globalizeObjectId(this.id, metaObjectData.parent); } } // Globalize MetaObject property set IDs if (globalize) { const propertySetIds = metaObjectData.propertySetIds; if (propertySetIds) { const propertySetGlobalIds = []; for (let j = 0, lenj = propertySetIds.length; j < lenj; j++) { propertySetGlobalIds.push(math.globalizeObjectId(this.id, propertySetIds[j])); } metaObjectData.propertySetIds = propertySetGlobalIds; metaObjectData.originalSystemPropertySetIds = propertySetIds; } } else { metaObjectData.originalSystemPropertySetIds = metaObjectData.propertySetIds; } } } // Globalize global PropertySet IDs if (metaModelData.propertySets) { for (let i = 0, len = metaModelData.propertySets.length; i < len; i++) { const propertySet = metaModelData.propertySets[i]; propertySet.originalSystemId = propertySet.id; if (globalize) { propertySet.id = math.globalizeObjectId(this.id, propertySet.id); } } } } } /** * @desc Metadata corresponding to a {@link Scene}. * * * Located in {@link Viewer#metaScene}. * * Contains {@link MetaModel}s and {@link MetaObject}s. * * [Scene graph example with metadata](http://xeokit.github.io/xeokit-sdk/examples/index.html#sceneRepresentation_SceneGraph_metadata) */ class MetaScene { /** * @private */ constructor(viewer, scene) { /** * The {@link Viewer}. * @property viewer * @type {Viewer} */ this.viewer = viewer; /** * The {@link Scene}. * @property scene * @type {Scene} */ this.scene = scene; /** * The {@link MetaModel}s belonging to this MetaScene, each mapped to its {@link MetaModel#modelId}. * * @type {{String:MetaModel}} */ this.metaModels = {}; /** * The {@link PropertySet}s belonging to this MetaScene, each mapped to its {@link PropertySet#id}. * * @type {{String:PropertySet}} */ this.propertySets = {}; /** * The {@link MetaObject}s belonging to this MetaScene, each mapped to its {@link MetaObject#id}. * * @type {{String:MetaObject}} */ this.metaObjects = {}; /** * The {@link MetaObject}s belonging to this MetaScene, each mapped to its {@link MetaObject#type}. * * @type {{String:MetaObject}} */ this.metaObjectsByType = {}; /** * The root {@link MetaObject}s belonging to this MetaScene, each mapped to its {@link MetaObject#id}. * * @type {{String:MetaObject}} */ this.rootMetaObjects = {}; /** * Subscriptions to events sent with {@link fire}. * @private */ this._eventSubs = {}; } /** * Subscribes to an event fired at this Viewer. * * @param {String} event The event * @param {Function} callback Callback fired on the event */ on(event, callback) { let subs = this._eventSubs[event]; if (!subs) { subs = []; this._eventSubs[event] = subs; } subs.push(callback); } /** * Fires an event at this Viewer. * * @param {String} event Event name * @param {Object} value Event parameters */ fire(event, value) { const subs = this._eventSubs[event]; if (subs) { for (let i = 0, len = subs.length; i < len; i++) { subs[i](value); } } } /** * Unsubscribes from an event fired at this Viewer. * @param event */ off(event) { // TODO } /** * Creates a {@link MetaModel} in this MetaScene. * * The MetaModel will contain a hierarchy of {@link MetaObject}s, created from the * meta objects in ````metaModelData````. * * The meta object hierarchy in ````metaModelData```` is expected to be non-cyclic, with a single root. If the meta * objects are cyclic, then this method will log an error and attempt to recover by creating a dummy root MetaObject * of type "Model" and connecting all other MetaObjects as its direct children. If the meta objects contain multiple * roots, then this method similarly attempts to recover by creating a dummy root MetaObject of type "Model" and * connecting all the root MetaObjects as its children. * * @param {String} modelId ID for the new {@link MetaModel}, which will have {@link MetaModel#id} set to this value. * @param {Object} metaModelData Data for the {@link MetaModel}. * @param {Object} [options] Options for creating the {@link MetaModel}. * @param {Boolean} [options.globalizeObjectIds=false] Whether to globalize each {@link MetaObject#id}. Set this ````true```` when you need to load multiple instances of the same meta model, to avoid ID clashes between the meta objects in the different instances. * @returns {MetaModel} The new MetaModel. */ createMetaModel(modelId, metaModelData, options = {}) { const metaModel = new MetaModel({ // Registers MetaModel in #metaModels metaScene: this, id: modelId, projectId: metaModelData.projectId || "none", revisionId: metaModelData.revisionId || "none", author: metaModelData.author || "none", createdAt: metaModelData.createdAt || "none", creatingApplication: metaModelData.creatingApplication || "none", schema: metaModelData.schema || "none", propertySets: [] }); metaModel.loadData(metaModelData); metaModel.finalize(); return metaModel; } /** * Removes a {@link MetaModel} from this MetaScene. * * Fires a "metaModelDestroyed" event with the value of the {@link MetaModel#id}. * * @param {String} metaModelId ID of the target {@link MetaModel}. */ destroyMetaModel(metaModelId) { const metaModel = this.metaModels[metaModelId]; if (!metaModel) { return; } // Remove global PropertySets if (metaModel.propertySets) { for (let i = 0, len = metaModel.propertySets.length; i < len; i++) { const propertySet = metaModel.propertySets[i]; if (propertySet.metaModels.length === 1 && propertySet.metaModels[0].id === metaModelId) { // Property set owned only by this model, delete delete this.propertySets[propertySet.id]; } else { const newMetaModels = []; for (let j = 0, lenj = propertySet.metaModels.length; j < lenj; j++) { if (propertySet.metaModels[j].id !== metaModelId) { newMetaModels.push(propertySet.metaModels[j]); } } propertySet.metaModels = newMetaModels; } } } // Remove MetaObjects if (metaModel.metaObjects) { for (let objectId in metaModel.metaObjects) { const metaObject = metaModel.metaObjects[objectId]; if (metaObject.metaModels.length === 1 && metaObject.metaModels[0].id === metaModelId) { // MetaObject owned only by this model, delete delete this.metaObjects[objectId]; if (!metaObject.parent) { delete this.rootMetaObjects[objectId]; } } else { metaObject.metaModels = metaObject.metaModels.filter(metaModel => metaModel.id !== metaModelId); } } } // Re-link entire MetaObject parent/child hierarchy for (let objectId in this.metaObjects) { const metaObject = this.metaObjects[objectId]; if (metaObject.children) { metaObject.children = []; } // Re-link each MetaObject's property sets if (metaObject.propertySets) { metaObject.propertySets = []; } if (metaObject.propertySetIds) { for (let i = 0, len = metaObject.propertySetIds.length; i < len; i++) { const propertySetId = metaObject.propertySetIds[i]; const propertySet = this.propertySets[propertySetId]; metaObject.propertySets.push(propertySet); } } } this.metaObjectsByType = {}; for (let objectId in this.metaObjects) { const metaObject = this.metaObjects[objectId]; const type = metaObject.type; if (metaObject.children) { metaObject.children = null; } (this.metaObjectsByType[type] || (this.metaObjectsByType[type] = {}))[objectId] = metaObject; } for (let objectId in this.metaObjects) { const metaObject = this.metaObjects[objectId]; if (metaObject.parentId) { const parentMetaObject = this.metaObjects[metaObject.parentId]; if (parentMetaObject) { metaObject.parent = parentMetaObject; (parentMetaObject.children || (parentMetaObject.children = [])).push(metaObject); } } } delete this.metaModels[metaModelId]; // Relink MetaObjects to their MetaModels for (let objectId in this.metaObjects) { const metaObject = this.metaObjects[objectId]; metaObject.metaModels = []; } for (let modelId in this.metaModels) { const metaModel = this.metaModels[modelId]; for (let objectId in metaModel.metaObjects) { const metaObject = metaModel.metaObjects[objectId]; metaObject.metaModels.push(metaModel); } } this.fire("metaModelDestroyed", metaModelId); } /** * Gets the {@link MetaObject#id}s of the {@link MetaObject}s that have the given {@link MetaObject#type}. * * @param {String} type The type. * @returns {String[]} Array of {@link MetaObject#id}s. */ getObjectIDsByType(type) { const metaObjects = this.metaObjectsByType[type]; return metaObjects ? Object.keys(metaObjects) : []; } /** * Gets the {@link MetaObject#id}s of the {@link MetaObject}s within the given subtree. * * @param {String} id ID of the root {@link MetaObject} of the given subtree. * @param {String[]} [includeTypes] Optional list of types to include. * @param {String[]} [excludeTypes] Optional list of types to exclude. * @returns {String[]} Array of {@link MetaObject#id}s. */ getObjectIDsInSubtree(id, includeTypes, excludeTypes) { const list = []; const metaObject = this.metaObjects[id]; const includeMask = (includeTypes && includeTypes.length > 0) ? arrayToMap(includeTypes) : null; const excludeMask = (excludeTypes && excludeTypes.length > 0) ? arrayToMap(excludeTypes) : null; const visit = (metaObject) => { if (!metaObject) { return; } var include = true; if (excludeMask && excludeMask[metaObject.type]) { include = false; } else if (includeMask && (!includeMask[metaObject.type])) { include = false; } if (include) { list.push(metaObject.id); } const children = metaObject.children; if (children) { for (var i = 0, len = children.length; i < len; i++) { visit(children[i]); } } }; visit(metaObject); return list; } /** * Iterates over the {@link MetaObject}s within the subtree. * * @param {String} id ID of root {@link MetaObject}. * @param {Function} callback Callback fired at each {@link MetaObject}. */ withMetaObjectsInSubtree(id, callback) { const metaObject = this.metaObjects[id]; if (!metaObject) { return; } metaObject.withMetaObjectsInSubtree(callback); } } function arrayToMap(array) { const map = {}; for (var i = 0, len = array.length; i < len; i++) { map[array[i]] = true; } return map; } /*! * html2canvas 1.4.1 * Copyright (c) 2022 Niklas von Hertzen * Released under MIT License */ /*! ***************************************************************************** Copyright (c) Microsoft Corporation. Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted. THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. ***************************************************************************** */ /* global Reflect, Promise */ var extendStatics = function(d, b) { extendStatics = Object.setPrototypeOf || ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; }; return extendStatics(d, b); }; function __extends(d, b) { if (typeof b !== "function" && b !== null) throw new TypeError("Class extends value " + String(b) + " is not a constructor or null"); extendStatics(d, b); function __() { this.constructor = d; } d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); } var __assign = function() { __assign = Object.assign || function __assign(t) { for (var s, i = 1, n = arguments.length; i < n; i++) { s = arguments[i]; for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p]; } return t; }; return __assign.apply(this, arguments); }; function __awaiter(thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); } function __generator(thisArg, body) { var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g; return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; function verb(n) { return function (v) { return step([n, v]); }; } function step(op) { if (f) throw new TypeError("Generator is already executing."); while (_) try { if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; if (y = 0, t) op = [op[0] & 2, t.value]; switch (op[0]) { case 0: case 1: t = op; break; case 4: _.label++; return { value: op[1], done: false }; case 5: _.label++; y = op[1]; op = [0]; continue; case 7: op = _.ops.pop(); _.trys.pop(); continue; default: if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } if (t[2]) _.ops.pop(); _.trys.pop(); continue; } op = body.call(thisArg, _); } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; } } function __spreadArray(to, from, pack) { if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) { if (ar || !(i in from)) { if (!ar) ar = Array.prototype.slice.call(from, 0, i); ar[i] = from[i]; } } return to.concat(ar || from); } var Bounds = /** @class */ (function () { function Bounds(left, top, width, height) { this.left = left; this.top = top; this.width = width; this.height = height; } Bounds.prototype.add = function (x, y, w, h) { return new Bounds(this.left + x, this.top + y, this.width + w, this.height + h); }; Bounds.fromClientRect = function (context, clientRect) { return new Bounds(clientRect.left + context.windowBounds.left, clientRect.top + context.windowBounds.top, clientRect.width, clientRect.height); }; Bounds.fromDOMRectList = function (context, domRectList) { var domRect = Array.from(domRectList).find(function (rect) { return rect.width !== 0; }); return domRect ? new Bounds(domRect.left + context.windowBounds.left, domRect.top + context.windowBounds.top, domRect.width, domRect.height) : Bounds.EMPTY; }; Bounds.EMPTY = new Bounds(0, 0, 0, 0); return Bounds; }()); var parseBounds = function (context, node) { return Bounds.fromClientRect(context, node.getBoundingClientRect()); }; var parseDocumentSize = function (document) { var body = document.body; var documentElement = document.documentElement; if (!body || !documentElement) { throw new Error("Unable to get document size"); } var width = Math.max(Math.max(body.scrollWidth, documentElement.scrollWidth), Math.max(body.offsetWidth, documentElement.offsetWidth), Math.max(body.clientWidth, documentElement.clientWidth)); var height = Math.max(Math.max(body.scrollHeight, documentElement.scrollHeight), Math.max(body.offsetHeight, documentElement.offsetHeight), Math.max(body.clientHeight, documentElement.clientHeight)); return new Bounds(0, 0, width, height); }; /* * css-line-break 2.1.0 * Copyright (c) 2022 Niklas von Hertzen * Released under MIT License */ var toCodePoints$1 = function (str) { var codePoints = []; var i = 0; var length = str.length; while (i < length) { var value = str.charCodeAt(i++); if (value >= 0xd800 && value <= 0xdbff && i < length) { var extra = str.charCodeAt(i++); if ((extra & 0xfc00) === 0xdc00) { codePoints.push(((value & 0x3ff) << 10) + (extra & 0x3ff) + 0x10000); } else { codePoints.push(value); i--; } } else { codePoints.push(value); } } return codePoints; }; var fromCodePoint$1 = function () { var codePoints = []; for (var _i = 0; _i < arguments.length; _i++) { codePoints[_i] = arguments[_i]; } if (String.fromCodePoint) { return String.fromCodePoint.apply(String, codePoints); } var length = codePoints.length; if (!length) { return ''; } var codeUnits = []; var index = -1; var result = ''; while (++index < length) { var codePoint = codePoints[index]; if (codePoint <= 0xffff) { codeUnits.push(codePoint); } else { codePoint -= 0x10000; codeUnits.push((codePoint >> 10) + 0xd800, (codePoint % 0x400) + 0xdc00); } if (index + 1 === length || codeUnits.length > 0x4000) { result += String.fromCharCode.apply(String, codeUnits); codeUnits.length = 0; } } return result; }; var chars$2 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'; // Use a lookup table to find the index. var lookup$2 = typeof Uint8Array === 'undefined' ? [] : new Uint8Array(256); for (var i$2 = 0; i$2 < chars$2.length; i$2++) { lookup$2[chars$2.charCodeAt(i$2)] = i$2; } /* * utrie 1.0.2 * Copyright (c) 2022 Niklas von Hertzen * Released under MIT License */ var chars$1$1 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'; // Use a lookup table to find the index. var lookup$1$1 = typeof Uint8Array === 'undefined' ? [] : new Uint8Array(256); for (var i$1$1 = 0; i$1$1 < chars$1$1.length; i$1$1++) { lookup$1$1[chars$1$1.charCodeAt(i$1$1)] = i$1$1; } var decode$1$1 = function (base64) { var bufferLength = base64.length * 0.75, len = base64.length, i, p = 0, encoded1, encoded2, encoded3, encoded4; if (base64[base64.length - 1] === '=') { bufferLength--; if (base64[base64.length - 2] === '=') { bufferLength--; } } var buffer = typeof ArrayBuffer !== 'undefined' && typeof Uint8Array !== 'undefined' && typeof Uint8Array.prototype.slice !== 'undefined' ? new ArrayBuffer(bufferLength) : new Array(bufferLength); var bytes = Array.isArray(buffer) ? buffer : new Uint8Array(buffer); for (i = 0; i < len; i += 4) { encoded1 = lookup$1$1[base64.charCodeAt(i)]; encoded2 = lookup$1$1[base64.charCodeAt(i + 1)]; encoded3 = lookup$1$1[base64.charCodeAt(i + 2)]; encoded4 = lookup$1$1[base64.charCodeAt(i + 3)]; bytes[p++] = (encoded1 << 2) | (encoded2 >> 4); bytes[p++] = ((encoded2 & 15) << 4) | (encoded3 >> 2); bytes[p++] = ((encoded3 & 3) << 6) | (encoded4 & 63); } return buffer; }; var polyUint16Array$1 = function (buffer) { var length = buffer.length; var bytes = []; for (var i = 0; i < length; i += 2) { bytes.push((buffer[i + 1] << 8) | buffer[i]); } return bytes; }; var polyUint32Array$1 = function (buffer) { var length = buffer.length; var bytes = []; for (var i = 0; i < length; i += 4) { bytes.push((buffer[i + 3] << 24) | (buffer[i + 2] << 16) | (buffer[i + 1] << 8) | buffer[i]); } return bytes; }; /** Shift size for getting the index-2 table offset. */ var UTRIE2_SHIFT_2$1 = 5; /** Shift size for getting the index-1 table offset. */ var UTRIE2_SHIFT_1$1 = 6 + 5; /** * Shift size for shifting left the index array values. * Increases possible data size with 16-bit index values at the cost * of compactability. * This requires data blocks to be aligned by UTRIE2_DATA_GRANULARITY. */ var UTRIE2_INDEX_SHIFT$1 = 2; /** * Difference between the two shift sizes, * for getting an index-1 offset from an index-2 offset. 6=11-5 */ var UTRIE2_SHIFT_1_2$1 = UTRIE2_SHIFT_1$1 - UTRIE2_SHIFT_2$1; /** * The part of the index-2 table for U+D800..U+DBFF stores values for * lead surrogate code _units_ not code _points_. * Values for lead surrogate code _points_ are indexed with this portion of the table. * Length=32=0x20=0x400>>UTRIE2_SHIFT_2. (There are 1024=0x400 lead surrogates.) */ var UTRIE2_LSCP_INDEX_2_OFFSET$1 = 0x10000 >> UTRIE2_SHIFT_2$1; /** Number of entries in a data block. 32=0x20 */ var UTRIE2_DATA_BLOCK_LENGTH$1 = 1 << UTRIE2_SHIFT_2$1; /** Mask for getting the lower bits for the in-data-block offset. */ var UTRIE2_DATA_MASK$1 = UTRIE2_DATA_BLOCK_LENGTH$1 - 1; var UTRIE2_LSCP_INDEX_2_LENGTH$1 = 0x400 >> UTRIE2_SHIFT_2$1; /** Count the lengths of both BMP pieces. 2080=0x820 */ var UTRIE2_INDEX_2_BMP_LENGTH$1 = UTRIE2_LSCP_INDEX_2_OFFSET$1 + UTRIE2_LSCP_INDEX_2_LENGTH$1; /** * The 2-byte UTF-8 version of the index-2 table follows at offset 2080=0x820. * Length 32=0x20 for lead bytes C0..DF, regardless of UTRIE2_SHIFT_2. */ var UTRIE2_UTF8_2B_INDEX_2_OFFSET$1 = UTRIE2_INDEX_2_BMP_LENGTH$1; var UTRIE2_UTF8_2B_INDEX_2_LENGTH$1 = 0x800 >> 6; /* U+0800 is the first code point after 2-byte UTF-8 */ /** * The index-1 table, only used for supplementary code points, at offset 2112=0x840. * Variable length, for code points up to highStart, where the last single-value range starts. * Maximum length 512=0x200=0x100000>>UTRIE2_SHIFT_1. * (For 0x100000 supplementary code points U+10000..U+10ffff.) * * The part of the index-2 table for supplementary code points starts * after this index-1 table. * * Both the index-1 table and the following part of the index-2 table * are omitted completely if there is only BMP data. */ var UTRIE2_INDEX_1_OFFSET$1 = UTRIE2_UTF8_2B_INDEX_2_OFFSET$1 + UTRIE2_UTF8_2B_INDEX_2_LENGTH$1; /** * Number of index-1 entries for the BMP. 32=0x20 * This part of the index-1 table is omitted from the serialized form. */ var UTRIE2_OMITTED_BMP_INDEX_1_LENGTH$1 = 0x10000 >> UTRIE2_SHIFT_1$1; /** Number of entries in an index-2 block. 64=0x40 */ var UTRIE2_INDEX_2_BLOCK_LENGTH$1 = 1 << UTRIE2_SHIFT_1_2$1; /** Mask for getting the lower bits for the in-index-2-block offset. */ var UTRIE2_INDEX_2_MASK$1 = UTRIE2_INDEX_2_BLOCK_LENGTH$1 - 1; var slice16$1 = function (view, start, end) { if (view.slice) { return view.slice(start, end); } return new Uint16Array(Array.prototype.slice.call(view, start, end)); }; var slice32$1 = function (view, start, end) { if (view.slice) { return view.slice(start, end); } return new Uint32Array(Array.prototype.slice.call(view, start, end)); }; var createTrieFromBase64$1 = function (base64, _byteLength) { var buffer = decode$1$1(base64); var view32 = Array.isArray(buffer) ? polyUint32Array$1(buffer) : new Uint32Array(buffer); var view16 = Array.isArray(buffer) ? polyUint16Array$1(buffer) : new Uint16Array(buffer); var headerLength = 24; var index = slice16$1(view16, headerLength / 2, view32[4] / 2); var data = view32[5] === 2 ? slice16$1(view16, (headerLength + view32[4]) / 2) : slice32$1(view32, Math.ceil((headerLength + view32[4]) / 4)); return new Trie$1(view32[0], view32[1], view32[2], view32[3], index, data); }; var Trie$1 = /** @class */ (function () { function Trie(initialValue, errorValue, highStart, highValueIndex, index, data) { this.initialValue = initialValue; this.errorValue = errorValue; this.highStart = highStart; this.highValueIndex = highValueIndex; this.index = index; this.data = data; } /** * Get the value for a code point as stored in the Trie. * * @param codePoint the code point * @return the value */ Trie.prototype.get = function (codePoint) { var ix; if (codePoint >= 0) { if (codePoint < 0x0d800 || (codePoint > 0x0dbff && codePoint <= 0x0ffff)) { // Ordinary BMP code point, excluding leading surrogates. // BMP uses a single level lookup. BMP index starts at offset 0 in the Trie2 index. // 16 bit data is stored in the index array itself. ix = this.index[codePoint >> UTRIE2_SHIFT_2$1]; ix = (ix << UTRIE2_INDEX_SHIFT$1) + (codePoint & UTRIE2_DATA_MASK$1); return this.data[ix]; } if (codePoint <= 0xffff) { // Lead Surrogate Code Point. A Separate index section is stored for // lead surrogate code units and code points. // The main index has the code unit data. // For this function, we need the code point data. // Note: this expression could be refactored for slightly improved efficiency, but // surrogate code points will be so rare in practice that it's not worth it. ix = this.index[UTRIE2_LSCP_INDEX_2_OFFSET$1 + ((codePoint - 0xd800) >> UTRIE2_SHIFT_2$1)]; ix = (ix << UTRIE2_INDEX_SHIFT$1) + (codePoint & UTRIE2_DATA_MASK$1); return this.data[ix]; } if (codePoint < this.highStart) { // Supplemental code point, use two-level lookup. ix = UTRIE2_INDEX_1_OFFSET$1 - UTRIE2_OMITTED_BMP_INDEX_1_LENGTH$1 + (codePoint >> UTRIE2_SHIFT_1$1); ix = this.index[ix]; ix += (codePoint >> UTRIE2_SHIFT_2$1) & UTRIE2_INDEX_2_MASK$1; ix = this.index[ix]; ix = (ix << UTRIE2_INDEX_SHIFT$1) + (codePoint & UTRIE2_DATA_MASK$1); return this.data[ix]; } if (codePoint <= 0x10ffff) { return this.data[this.highValueIndex]; } } // Fall through. The code point is outside of the legal range of 0..0x10ffff. return this.errorValue; }; return Trie; }()); /* * base64-arraybuffer 1.0.2 * Copyright (c) 2022 Niklas von Hertzen * Released under MIT License */ var chars$3 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'; // Use a lookup table to find the index. var lookup$3 = typeof Uint8Array === 'undefined' ? [] : new Uint8Array(256); for (var i$3 = 0; i$3 < chars$3.length; i$3++) { lookup$3[chars$3.charCodeAt(i$3)] = i$3; } var base64$1 = 'KwAAAAAAAAAACA4AUD0AADAgAAACAAAAAAAIABAAGABAAEgAUABYAGAAaABgAGgAYgBqAF8AZwBgAGgAcQB5AHUAfQCFAI0AlQCdAKIAqgCyALoAYABoAGAAaABgAGgAwgDKAGAAaADGAM4A0wDbAOEA6QDxAPkAAQEJAQ8BFwF1AH0AHAEkASwBNAE6AUIBQQFJAVEBWQFhAWgBcAF4ATAAgAGGAY4BlQGXAZ8BpwGvAbUBvQHFAc0B0wHbAeMB6wHxAfkBAQIJAvEBEQIZAiECKQIxAjgCQAJGAk4CVgJeAmQCbAJ0AnwCgQKJApECmQKgAqgCsAK4ArwCxAIwAMwC0wLbAjAA4wLrAvMC+AIAAwcDDwMwABcDHQMlAy0DNQN1AD0DQQNJA0kDSQNRA1EDVwNZA1kDdQB1AGEDdQBpA20DdQN1AHsDdQCBA4kDkQN1AHUAmQOhA3UAdQB1AHUAdQB1AHUAdQB1AHUAdQB1AHUAdQB1AHUAdQB1AKYDrgN1AHUAtgO+A8YDzgPWAxcD3gPjA+sD8wN1AHUA+wMDBAkEdQANBBUEHQQlBCoEFwMyBDgEYABABBcDSARQBFgEYARoBDAAcAQzAXgEgASIBJAEdQCXBHUAnwSnBK4EtgS6BMIEyAR1AHUAdQB1AHUAdQCVANAEYABgAGAAYABgAGAAYABgANgEYADcBOQEYADsBPQE/AQEBQwFFAUcBSQFLAU0BWQEPAVEBUsFUwVbBWAAYgVgAGoFcgV6BYIFigWRBWAAmQWfBaYFYABgAGAAYABgAKoFYACxBbAFuQW6BcEFwQXHBcEFwQXPBdMF2wXjBeoF8gX6BQIGCgYSBhoGIgYqBjIGOgZgAD4GRgZMBmAAUwZaBmAAYABgAGAAYABgAGAAYABgAGAAYABgAGIGYABpBnAGYABgAGAAYABgAGAAYABgAGAAYAB4Bn8GhQZgAGAAYAB1AHcDFQSLBmAAYABgAJMGdQA9A3UAmwajBqsGqwaVALMGuwbDBjAAywbSBtIG1QbSBtIG0gbSBtIG0gbdBuMG6wbzBvsGAwcLBxMHAwcbByMHJwcsBywHMQcsB9IGOAdAB0gHTgfSBkgHVgfSBtIG0gbSBtIG0gbSBtIG0gbSBiwHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAdgAGAALAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAdbB2MHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsB2kH0gZwB64EdQB1AHUAdQB1AHUAdQB1AHUHfQdgAIUHjQd1AHUAlQedB2AAYAClB6sHYACzB7YHvgfGB3UAzgfWBzMB3gfmB1EB7gf1B/0HlQENAQUIDQh1ABUIHQglCBcDLQg1CD0IRQhNCEEDUwh1AHUAdQBbCGMIZAhlCGYIZwhoCGkIYwhkCGUIZghnCGgIaQhjCGQIZQhmCGcIaAhpCGMIZAhlCGYIZwhoCGkIYwhkCGUIZghnCGgIaQhjCGQIZQhmCGcIaAhpCGMIZAhlCGYIZwhoCGkIYwhkCGUIZghnCGgIaQhjCGQIZQhmCGcIaAhpCGMIZAhlCGYIZwhoCGkIYwhkCGUIZghnCGgIaQhjCGQIZQhmCGcIaAhpCGMIZAhlCGYIZwhoCGkIYwhkCGUIZghnCGgIaQhjCGQIZQhmCGcIaAhpCGMIZAhlCGYIZwhoCGkIYwhkCGUIZghnCGgIaQhjCGQIZQhmCGcIaAhpCGMIZAhlCGYIZwhoCGkIYwhkCGUIZghnCGgIaQhjCGQIZQhmCGcIaAhpCGMIZAhlCGYIZwhoCGkIYwhkCGUIZghnCGgIaQhjCGQIZQhmCGcIaAhpCGMIZAhlCGYIZwhoCGkIYwhkCGUIZghnCGgIaQhjCGQIZQhmCGcIaAhpCGMIZAhlCGYIZwhoCGkIYwhkCGUIZghnCGgIaQhjCGQIZQhmCGcIaAhpCGMIZAhlCGYIZwhoCGkIYwhkCGUIZghnCGgIaQhjCGQIZQhmCGcIaAhpCGMIZAhlCGYIZwhoCGkIYwhkCGUIZghnCGgIaQhjCGQIZQhmCGcIaAhpCGMIZAhlCGYIZwhoCGkIYwhkCGUIZghnCGgIaQhjCGQIZQhmCGcIaAhpCGMIZAhlCGYIZwhoCGkIYwhkCGUIZghnCGgIaQhjCGQIZQhmCGcIaAhpCGMIZAhlCGYIZwhoCGkIYwhkCGUIZghnCGgIaQhjCGQIZQhmCGcIaAhpCGMIZAhlCGYIZwhoCGkIYwhkCGUIZghnCGgIaQhjCGQIZQhmCGcIaAhpCGMIZAhlCGYIZwhoCGkIYwhkCGUIZghnCGgIcAh3CHoIMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwAIIIggiCCIIIggiCCIIIggiCCIIIggiCCIIIggiCCIIIggiCCIIIggiCCIIIggiCCIIIggiCCIIIggiCCIIIgggwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAALAcsBywHLAcsBywHLAcsBywHLAcsB4oILAcsB44I0gaWCJ4Ipgh1AHUAqgiyCHUAdQB1AHUAdQB1AHUAdQB1AHUAtwh8AXUAvwh1AMUIyQjRCNkI4AjoCHUAdQB1AO4I9gj+CAYJDgkTCS0HGwkjCYIIggiCCIIIggiCCIIIggiCCIIIggiCCIIIggiCCIIIggiCCIIIggiCCIIIggiCCIIIggiCCIIIggiCCIIIggiAAIAAAAFAAYABgAGIAXwBgAHEAdQBFAJUAogCyAKAAYABgAEIA4ABGANMA4QDxAMEBDwE1AFwBLAE6AQEBUQF4QkhCmEKoQrhCgAHIQsAB0MLAAcABwAHAAeDC6ABoAHDCwMMAAcABwAHAAdDDGMMAAcAB6MM4wwjDWMNow3jDaABoAGgAaABoAGgAaABoAGgAaABoAGgAaABoAGgAaABoAGgAaABoAEjDqABWw6bDqABpg6gAaABoAHcDvwOPA+gAaABfA/8DvwO/A78DvwO/A78DvwO/A78DvwO/A78DvwO/A78DvwO/A78DvwO/A78DvwO/A78DvwO/A78DpcPAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcAB9cPKwkyCToJMAB1AHUAdQBCCUoJTQl1AFUJXAljCWcJawkwADAAMAAwAHMJdQB2CX4JdQCECYoJjgmWCXUAngkwAGAAYABxAHUApgn3A64JtAl1ALkJdQDACTAAMAAwADAAdQB1AHUAdQB1AHUAdQB1AHUAowYNBMUIMAAwADAAMADICcsJ0wnZCRUE4QkwAOkJ8An4CTAAMAB1AAAKvwh1AAgKDwoXCh8KdQAwACcKLgp1ADYKqAmICT4KRgowADAAdQB1AE4KMAB1AFYKdQBeCnUAZQowADAAMAAwADAAMAAwADAAMAAVBHUAbQowADAAdQC5CXUKMAAwAHwBxAijBogEMgF9CoQKiASMCpQKmgqIBKIKqgquCogEDQG2Cr4KxgrLCjAAMADTCtsKCgHjCusK8Qr5CgELMAAwADAAMAB1AIsECQsRC3UANAEZCzAAMAAwADAAMAB1ACELKQswAHUANAExCzkLdQBBC0kLMABRC1kLMAAwADAAMAAwADAAdQBhCzAAMAAwAGAAYABpC3ELdwt/CzAAMACHC4sLkwubC58Lpwt1AK4Ltgt1APsDMAAwADAAMAAwADAAMAAwAL4LwwvLC9IL1wvdCzAAMADlC+kL8Qv5C/8LSQswADAAMAAwADAAMAAwADAAMAAHDDAAMAAwADAAMAAODBYMHgx1AHUAdQB1AHUAdQB1AHUAdQB1AHUAdQB1AHUAdQB1AHUAdQB1AHUAdQB1AHUAdQB1AHUAdQB1ACYMMAAwADAAdQB1AHUALgx1AHUAdQB1AHUAdQA2DDAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwAHUAdQB1AHUAdQB1AHUAdQB1AHUAdQB1AHUAdQB1AHUAdQB1AD4MdQBGDHUAdQB1AHUAdQB1AEkMdQB1AHUAdQB1AFAMMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwAHUAdQB1AHUAdQB1AHUAdQB1AHUAdQB1AHUAdQBYDHUAdQB1AF8MMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAB1AHUAdQB1AHUAdQB1AHUAdQB1AHUAdQB1AHUAdQB1AHUA+wMVBGcMMAAwAHwBbwx1AHcMfwyHDI8MMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAYABgAJcMMAAwADAAdQB1AJ8MlQClDDAAMACtDCwHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsB7UMLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHdQB1AHUAdQB1AHUAdQB1AHUAdQB1AHUAdQB1AA0EMAC9DDAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAsBywHLAcsBywHLAcsBywHLQcwAMEMyAwsBywHLAcsBywHLAcsBywHLAcsBywHzAwwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwAHUAdQB1ANQM2QzhDDAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMABgAGAAYABgAGAAYABgAOkMYADxDGAA+AwADQYNYABhCWAAYAAODTAAMAAwADAAFg1gAGAAHg37AzAAMAAwADAAYABgACYNYAAsDTQNPA1gAEMNPg1LDWAAYABgAGAAYABgAGAAYABgAGAAUg1aDYsGVglhDV0NcQBnDW0NdQ15DWAAYABgAGAAYABgAGAAYABgAGAAYABgAGAAYABgAGAAlQCBDZUAiA2PDZcNMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAnw2nDTAAMAAwADAAMAAwAHUArw23DTAAMAAwADAAMAAwADAAMAAwADAAMAB1AL8NMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAB1AHUAdQB1AHUAdQDHDTAAYABgAM8NMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAA1w11ANwNMAAwAD0B5A0wADAAMAAwADAAMADsDfQN/A0EDgwOFA4wABsOMAAwADAAMAAwADAAMAAwANIG0gbSBtIG0gbSBtIG0gYjDigOwQUuDsEFMw7SBjoO0gbSBtIG0gbSBtIG0gbSBtIG0gbSBtIGQg5KDlIOVg7SBtIGXg5lDm0OdQ7SBtIGfQ6EDooOjQ6UDtIGmg6hDtIG0gaoDqwO0ga0DrwO0gZgAGAAYADEDmAAYAAkBtIGzA5gANIOYADaDokO0gbSBt8O5w7SBu8O0gb1DvwO0gZgAGAAxA7SBtIG0gbSBtIGYABgAGAAYAAED2AAsAUMD9IG0gbSBtIG0gbSBtIG0gbSBtIG0gbSBtIG0gbSBtIG0gbSBtIG0gbSBtIG0gbSBtIG0gbSBtIG0gbSBtIGFA8sBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAccD9IGLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHJA8sBywHLAcsBywHLAccDywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywPLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAc0D9IG0gbSBtIG0gbSBtIG0gbSBtIG0gbSBtIG0gbSBtIG0gbSBtIG0gbSBtIG0gbSBtIG0gbSBtIG0gbSBtIG0gbSBtIG0gbSBtIG0gbSBtIG0gbSBtIG0gbSBtIG0gbSBtIG0gbSBtIG0gbSBtIG0gbSBtIG0gbSBtIG0gbSBtIGLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAccD9IG0gbSBtIG0gbSBtIG0gbSBtIG0gbSBtIG0gbSBtIG0gbSBtIG0gbSBtIG0gbSBtIG0gbSBtIG0gbSBtIG0gbSBtIG0gbSBtIG0gbSBtIG0gbSBtIG0gbSBtIGFA8sBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHPA/SBtIG0gbSBtIG0gbSBtIG0gbSBtIG0gbSBtIG0gbSBtIG0gbSBtIG0gbSBtIG0gbSBtIG0gbSBtIG0gbSBtIG0gbSBtIG0gbSBtIG0gbSBtIG0gbSBtIG0gbSBtIG0gbSBtIG0gbSBtIG0gbSBtIG0gbSBtIG0gbSBtIG0gYUD0QPlQCVAJUAMAAwADAAMACVAJUAlQCVAJUAlQCVAEwPMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAA//8EAAQABAAEAAQABAAEAAQABAANAAMAAQABAAIABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQACgATABcAHgAbABoAHgAXABYAEgAeABsAGAAPABgAHABLAEsASwBLAEsASwBLAEsASwBLABgAGAAeAB4AHgATAB4AUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQABYAGwASAB4AHgAeAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUAAWAA0AEQAeAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArAAQABAAEAAQABAAFAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAJABYAGgAbABsAGwAeAB0AHQAeAE8AFwAeAA0AHgAeABoAGwBPAE8ADgBQAB0AHQAdAE8ATwAXAE8ATwBPABYAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAB0AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAdAFAAUABQAFAAUABQAFAAUAAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAFAAHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgBQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUAAeAB4AHgAeAFAATwBAAE8ATwBPAEAATwBQAFAATwBQAB4AHgAeAB4AHgAeAB0AHQAdAB0AHgAdAB4ADgBQAFAAUABQAFAAHgAeAB4AHgAeAB4AHgBQAB4AUAAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4ABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAJAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAkACQAJAAkACQAJAAkABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAeAB4AHgAeAFAAHgAeAB4AKwArAFAAUABQAFAAGABQACsAKwArACsAHgAeAFAAHgBQAFAAUAArAFAAKwAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AKwAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4ABAAEAAQABAAEAAQABAAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgArAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUAArACsAUAAeAB4AHgAeAB4AHgBQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUAAYAA0AKwArAB4AHgAbACsABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQADQAEAB4ABAAEAB4ABAAEABMABAArACsAKwArACsAKwArACsAVgBWAFYAVgBWAFYAVgBWAFYAVgBWAFYAVgBWAFYAVgBWAFYAVgBWAFYAVgBWAFYAVgBWAFYAKwArACsAKwBWAFYAVgBWAB4AHgArACsAKwArACsAKwArACsAKwArACsAHgAeAB4AHgAeAB4AHgAeAB4AGgAaABoAGAAYAB4AHgAEAAQABAAEAAQABAAEAAQABAAEAAQAEwAEACsAEwATAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABABLAEsASwBLAEsASwBLAEsASwBLABoAGQAZAB4AUABQAAQAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQABMAUAAEAAQABAAEAAQABAAEAB4AHgAEAAQABAAEAAQABABQAFAABAAEAB4ABAAEAAQABABQAFAASwBLAEsASwBLAEsASwBLAEsASwBQAFAAUAAeAB4AUAAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AKwAeAFAABABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEACsAKwBQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAABAAEAAQABAAEAAQABAAEAAQABAAEAFAAKwArACsAKwArACsAKwArACsAKwArACsAKwArAEsASwBLAEsASwBLAEsASwBLAEsAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAABAAEAAQABAAEAAQABAAEAAQAUABQAB4AHgAYABMAUAArACsABAAbABsAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUAAEAAQABAAEAFAABAAEAAQABAAEAFAABAAEAAQAUAAEAAQABAAEAAQAKwArAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeACsAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUAAEAAQABAArACsAHgArAFAAUABQAFAAUABQAFAAUABQAFAAUAArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwBQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUAArAFAAUABQAFAAUABQAFAAUABQAFAAKwArACsAKwArACsAKwArACsAKwArAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAB4ABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAAQABAAEAFAABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQAUAAEAAQABAAEAAQABAAEAFAAUABQAFAAUABQAFAAUABQAFAABAAEAA0ADQBLAEsASwBLAEsASwBLAEsASwBLAB4AUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUAAEAAQABAArAFAAUABQAFAAUABQAFAAUAArACsAUABQACsAKwBQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAKwBQAFAAUABQAFAAUABQACsAUAArACsAKwBQAFAAUABQACsAKwAEAFAABAAEAAQABAAEAAQABAArACsABAAEACsAKwAEAAQABABQACsAKwArACsAKwArACsAKwAEACsAKwArACsAUABQACsAUABQAFAABAAEACsAKwBLAEsASwBLAEsASwBLAEsASwBLAFAAUAAaABoAUABQAFAAUABQAEwAHgAbAFAAHgAEACsAKwAEAAQABAArAFAAUABQAFAAUABQACsAKwArACsAUABQACsAKwBQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAKwBQAFAAUABQAFAAUABQACsAUABQACsAUABQACsAUABQACsAKwAEACsABAAEAAQABAAEACsAKwArACsABAAEACsAKwAEAAQABAArACsAKwAEACsAKwArACsAKwArACsAUABQAFAAUAArAFAAKwArACsAKwArACsAKwBLAEsASwBLAEsASwBLAEsASwBLAAQABABQAFAAUAAEAB4AKwArACsAKwArACsAKwArACsAKwAEAAQABAArAFAAUABQAFAAUABQAFAAUABQACsAUABQAFAAKwBQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAKwBQAFAAUABQAFAAUABQACsAUABQACsAUABQAFAAUABQACsAKwAEAFAABAAEAAQABAAEAAQABAAEACsABAAEAAQAKwAEAAQABAArACsAUAArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwBQAFAABAAEACsAKwBLAEsASwBLAEsASwBLAEsASwBLAB4AGwArACsAKwArACsAKwArAFAABAAEAAQABAAEAAQAKwAEAAQABAArAFAAUABQAFAAUABQAFAAUAArACsAUABQACsAKwBQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAAQABAAEAAQABAArACsABAAEACsAKwAEAAQABAArACsAKwArACsAKwArAAQABAAEACsAKwArACsAUABQACsAUABQAFAABAAEACsAKwBLAEsASwBLAEsASwBLAEsASwBLAB4AUABQAFAAUABQAFAAUAArACsAKwArACsAKwArACsAKwArAAQAUAArAFAAUABQAFAAUABQACsAKwArAFAAUABQACsAUABQAFAAUAArACsAKwBQAFAAKwBQACsAUABQACsAKwArAFAAUAArACsAKwBQAFAAUAArACsAKwBQAFAAUABQAFAAUABQAFAAUABQAFAAUAArACsAKwArAAQABAAEAAQABAArACsAKwAEAAQABAArAAQABAAEAAQAKwArAFAAKwArACsAKwArACsABAArACsAKwArACsAKwArACsAKwArAEsASwBLAEsASwBLAEsASwBLAEsAUABQAFAAHgAeAB4AHgAeAB4AGwAeACsAKwArACsAKwAEAAQABAAEAAQAUABQAFAAUABQAFAAUABQACsAUABQAFAAKwBQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUAArAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAKwArACsAUAAEAAQABAAEAAQABAAEACsABAAEAAQAKwAEAAQABAAEACsAKwArACsAKwArACsABAAEACsAUABQAFAAKwArACsAKwArAFAAUAAEAAQAKwArAEsASwBLAEsASwBLAEsASwBLAEsAKwArACsAKwArACsAKwAOAFAAUABQAFAAUABQAFAAHgBQAAQABAAEAA4AUABQAFAAUABQAFAAUABQACsAUABQAFAAKwBQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUAArAFAAUABQAFAAUABQAFAAUABQAFAAKwBQAFAAUABQAFAAKwArAAQAUAAEAAQABAAEAAQABAAEACsABAAEAAQAKwAEAAQABAAEACsAKwArACsAKwArACsABAAEACsAKwArACsAKwArACsAUAArAFAAUAAEAAQAKwArAEsASwBLAEsASwBLAEsASwBLAEsAKwBQAFAAKwArACsAKwArACsAKwArACsAKwArACsAKwAEAAQABAAEAFAAUABQAFAAUABQAFAAUABQACsAUABQAFAAKwBQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAABAAEAFAABAAEAAQABAAEAAQABAArAAQABAAEACsABAAEAAQABABQAB4AKwArACsAKwBQAFAAUAAEAFAAUABQAFAAUABQAFAAUABQAFAABAAEACsAKwBLAEsASwBLAEsASwBLAEsASwBLAFAAUABQAFAAUABQAFAAUABQABoAUABQAFAAUABQAFAAKwAEAAQABAArAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQACsAKwArAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUAArAFAAUABQAFAAUABQAFAAUABQACsAUAArACsAUABQAFAAUABQAFAAUAArACsAKwAEACsAKwArACsABAAEAAQABAAEAAQAKwAEACsABAAEAAQABAAEAAQABAAEACsAKwArACsAKwArAEsASwBLAEsASwBLAEsASwBLAEsAKwArAAQABAAeACsAKwArACsAKwArACsAKwArACsAKwArAFwAXABcAFwAXABcAFwAXABcAFwAXABcAFwAXABcAFwAXABcAFwAXABcAFwAXABcAFwAXABcAFwAXABcAFwAXAAqAFwAXAAqACoAKgAqACoAKgAqACsAKwArACsAGwBcAFwAXABcAFwAXABcACoAKgAqACoAKgAqACoAKgAeAEsASwBLAEsASwBLAEsASwBLAEsADQANACsAKwArACsAKwBcAFwAKwBcACsAXABcAFwAXABcACsAXABcAFwAXABcAFwAXABcAFwAXABcAFwAXABcAFwAXABcAFwAXABcACsAXAArAFwAXABcAFwAXABcAFwAXABcAFwAKgBcAFwAKgAqACoAKgAqACoAKgAqACoAXAArACsAXABcAFwAXABcACsAXAArACoAKgAqACoAKgAqACsAKwBLAEsASwBLAEsASwBLAEsASwBLACsAKwBcAFwAXABcAFAADgAOAA4ADgAeAA4ADgAJAA4ADgANAAkAEwATABMAEwATAAkAHgATAB4AHgAeAAQABAAeAB4AHgAeAB4AHgBLAEsASwBLAEsASwBLAEsASwBLAFAAUABQAFAAUABQAFAAUABQAFAADQAEAB4ABAAeAAQAFgARABYAEQAEAAQAUABQAFAAUABQAFAAUABQACsAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAKwArACsAKwAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQADQAEAAQABAAEAAQADQAEAAQAUABQAFAAUABQAAQABAAEAAQABAAEAAQABAAEAAQABAArAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAArAA0ADQAeAB4AHgAeAB4AHgAEAB4AHgAeAB4AHgAeACsAHgAeAA4ADgANAA4AHgAeAB4AHgAeAAkACQArACsAKwArACsAXABcAFwAXABcAFwAXABcAFwAXABcAFwAXABcAFwAXABcAFwAXABcAFwAXABcAFwAXABcAFwAXABcAFwAXABcAFwAXABcACoAKgAqACoAKgAqACoAKgAqACoAKgAqACoAKgAqACoAKgAqACoAKgBcAEsASwBLAEsASwBLAEsASwBLAEsADQANAB4AHgAeAB4AXABcAFwAXABcAFwAKgAqACoAKgBcAFwAXABcACoAKgAqAFwAKgAqACoAXABcACoAKgAqACoAKgAqACoAXABcAFwAKgAqACoAKgBcAFwAXABcAFwAXABcAFwAXABcAFwAXABcACoAKgAqACoAKgAqACoAKgAqACoAKgAqAFwAKgBLAEsASwBLAEsASwBLAEsASwBLACoAKgAqACoAKgAqAFAAUABQAFAAUABQACsAUAArACsAKwArACsAUAArACsAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAHgBQAFAAUABQAFgAWABYAFgAWABYAFgAWABYAFgAWABYAFgAWABYAFgAWABYAFgAWABYAFgAWABYAFgAWABYAFgAWABYAFgAWABZAFkAWQBZAFkAWQBZAFkAWQBZAFkAWQBZAFkAWQBZAFkAWQBZAFkAWQBZAFkAWQBZAFkAWQBZAFkAWQBZAFkAWgBaAFoAWgBaAFoAWgBaAFoAWgBaAFoAWgBaAFoAWgBaAFoAWgBaAFoAWgBaAFoAWgBaAFoAWgBaAFoAWgBaAFAAUABQAFAAUABQAFAAUABQACsAUABQAFAAUAArACsAUABQAFAAUABQAFAAUAArAFAAKwBQAFAAUABQACsAKwBQAFAAUABQAFAAUABQAFAAUAArAFAAUABQAFAAKwArAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUAArAFAAUABQAFAAKwArAFAAUABQAFAAUABQAFAAKwBQACsAUABQAFAAUAArACsAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAKwBQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAKwBQAFAAUABQACsAKwBQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUAArACsABAAEAAQAHgANAB4AHgAeAB4AHgAeAB4AUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQACsAKwArAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAHgAeAB4AHgAeAB4AHgAeAB4AHgArACsAKwArACsAKwBQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQACsAKwBQAFAAUABQAFAAUAArACsADQBQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAHgAeAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUAANAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUAAWABEAKwArACsAUABQAFAAUABQAFAAUABQAFAAUABQAA0ADQANAFAAUABQAFAAUABQAFAAUABQAFAAUAArACsAKwArACsAKwArAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAKwBQAFAAUABQAAQABAAEACsAKwArACsAKwArACsAKwArACsAKwBQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUAAEAAQABAANAA0AKwArACsAKwArACsAKwArACsAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAABAAEACsAKwArACsAKwArACsAKwArACsAKwArAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAKwBQAFAAUAArAAQABAArACsAKwArACsAKwArACsAKwArACsAKwBcAFwAXABcAFwAXABcAFwAXABcAFwAXABcAFwAXABcAFwAXABcAFwAKgAqACoAKgAqACoAKgAqACoAKgAqACoAKgAqACoAKgAqACoAKgAqAA0ADQAVAFwADQAeAA0AGwBcACoAKwArAEsASwBLAEsASwBLAEsASwBLAEsAKwArACsAKwArACsAUABQAFAAUABQAFAAUABQAFAAUAArACsAKwArACsAKwAeAB4AEwATAA0ADQAOAB4AEwATAB4ABAAEAAQACQArAEsASwBLAEsASwBLAEsASwBLAEsAKwArACsAKwArACsAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUAArACsAKwArACsAKwArAFAAUABQAFAAUAAEAAQAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAAQAUAArACsAKwArACsAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUAArACsAKwArACsAKwArACsAKwArAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAKwAEAAQABAAEAAQABAAEAAQABAAEAAQABAArACsAKwArAAQABAAEAAQABAAEAAQABAAEAAQABAAEACsAKwArACsAHgArACsAKwATABMASwBLAEsASwBLAEsASwBLAEsASwBcAFwAXABcAFwAXABcAFwAXABcAFwAXABcAFwAXABcAFwAXAArACsAXABcAFwAXABcACsAKwArACsAKwArACsAKwArACsAKwBcAFwAXABcAFwAXABcAFwAXABcAFwAXAArACsAKwArAFwAXABcAFwAXABcAFwAXABcAFwAXABcAFwAXABcAFwAXABcACsAKwArACsAKwArAEsASwBLAEsASwBLAEsASwBLAEsAXAArACsAKwAqACoAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAAQABAAEAAQABAArACsAHgAeAFwAXABcAFwAXABcAFwAXABcAFwAXABcAFwAXABcAFwAXABcAFwAXABcACoAKgAqACoAKgAqACoAKgAqACoAKwAqACoAKgAqACoAKgAqACoAKgAqACoAKgAqACoAKgAqACoAKgAqACoAKgAqACoAKgAqACoAKgAqACoAKwArAAQASwBLAEsASwBLAEsASwBLAEsASwArACsAKwArACsAKwBLAEsASwBLAEsASwBLAEsASwBLACsAKwArACsAKwArACoAKgAqACoAKgAqACoAXAAqACoAKgAqACoAKgArACsABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsABAAEAAQABAAEAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAAQABAAEAAQABABQAFAAUABQAFAAUABQACsAKwArACsASwBLAEsASwBLAEsASwBLAEsASwANAA0AHgANAA0ADQANAB4AHgAeAB4AHgAeAB4AHgAeAB4ABAAEAAQABAAEAAQABAAEAAQAHgAeAB4AHgAeAB4AHgAeAB4AKwArACsABAAEAAQAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAABAAEAAQABAAEAAQABAAEAAQABAAEAAQABABQAFAASwBLAEsASwBLAEsASwBLAEsASwBQAFAAUABQAFAAUABQAFAABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEACsAKwArACsAKwArACsAKwAeAB4AHgAeAFAAUABQAFAABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEACsAKwArAA0ADQANAA0ADQBLAEsASwBLAEsASwBLAEsASwBLACsAKwArAFAAUABQAEsASwBLAEsASwBLAEsASwBLAEsAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAA0ADQBQAFAAUABQAFAAUABQAFAAUAArACsAKwArACsAKwArAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQACsAKwBQAFAAUAAeAB4AHgAeAB4AHgAeAB4AKwArACsAKwArACsAKwArAAQABAAEAB4ABAAEAAQABAAEAAQABAAEAAQABAAEAAQABABQAFAAUABQAAQAUABQAFAAUABQAFAABABQAFAABAAEAAQAUAArACsAKwArACsABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEACsABAAEAAQABAAEAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AKwArAFAAUABQAFAAUABQACsAKwBQAFAAUABQAFAAUABQAFAAKwBQACsAUAArAFAAKwAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeACsAKwAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgArAB4AHgAeAB4AHgAeAB4AHgBQAB4AHgAeAFAAUABQACsAHgAeAB4AHgAeAB4AHgAeAB4AHgBQAFAAUABQACsAKwAeAB4AHgAeAB4AHgArAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AKwArAFAAUABQACsAHgAeAB4AHgAeAB4AHgAOAB4AKwANAA0ADQANAA0ADQANAAkADQANAA0ACAAEAAsABAAEAA0ACQANAA0ADAAdAB0AHgAXABcAFgAXABcAFwAWABcAHQAdAB4AHgAUABQAFAANAAEAAQAEAAQABAAEAAQACQAaABoAGgAaABoAGgAaABoAHgAXABcAHQAVABUAHgAeAB4AHgAeAB4AGAAWABEAFQAVABUAHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4ADQAeAA0ADQANAA0AHgANAA0ADQAHAB4AHgAeAB4AKwAEAAQABAAEAAQABAAEAAQABAAEAFAAUAArACsATwBQAFAAUABQAFAAHgAeAB4AFgARAE8AUABPAE8ATwBPAFAAUABQAFAAUAAeAB4AHgAWABEAKwBQAFAAUABQAFAAUABQAFAAUABQAFAAUABQACsAKwArABsAGwAbABsAGwAbABsAGgAbABsAGwAbABsAGwAbABsAGwAbABsAGwAbABsAGgAbABsAGwAbABoAGwAbABoAGwAbABsAGwAbABsAGwAbABsAGwAbABsAGwAbABsAGwAbAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQAHgAeAFAAGgAeAB0AHgBQAB4AGgAeAB4AHgAeAB4AHgAeAB4AHgBPAB4AUAAbAB4AHgBQAFAAUABQAFAAHgAeAB4AHQAdAB4AUAAeAFAAHgBQAB4AUABPAFAAUAAeAB4AHgAeAB4AHgAeAFAAUABQAFAAUAAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAFAAHgBQAFAAUABQAE8ATwBQAFAAUABQAFAATwBQAFAATwBQAE8ATwBPAE8ATwBPAE8ATwBPAE8ATwBPAFAAUABQAFAATwBPAE8ATwBPAE8ATwBPAE8ATwBQAFAAUABQAFAAUABQAFAAUAAeAB4AUABQAFAAUABPAB4AHgArACsAKwArAB0AHQAdAB0AHQAdAB0AHQAdAB0AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB0AHgAdAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAdAB4AHQAdAB4AHgAeAB0AHQAeAB4AHQAeAB4AHgAdAB4AHQAbABsAHgAdAB4AHgAeAB4AHQAeAB4AHQAdAB0AHQAeAB4AHQAeAB0AHgAdAB0AHQAdAB0AHQAeAB0AHgAeAB4AHgAeAB0AHQAdAB0AHgAeAB4AHgAdAB0AHgAeAB4AHgAeAB4AHgAeAB4AHgAdAB4AHgAeAB0AHgAeAB4AHgAeAB0AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAdAB0AHgAeAB0AHQAdAB0AHgAeAB0AHQAeAB4AHQAdAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB0AHQAeAB4AHQAdAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHQAeAB4AHgAdAB4AHgAeAB4AHgAeAB4AHQAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB0AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AFAAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeABYAEQAWABEAHgAeAB4AHgAeAB4AHQAeAB4AHgAeAB4AHgAeACUAJQAeAB4AHgAeAB4AHgAeAB4AHgAWABEAHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AJQAlACUAJQAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArAE8ATwBPAE8ATwBPAE8ATwBPAE8ATwBPAE8ATwBPAE8ATwBPAE8ATwBPAE8ATwBPAE8ATwBPAE8ATwBPAE8ATwAdAB0AHQAdAB0AHQAdAB0AHQAdAB0AHQAdAB0AHQAdAB0AHQAdAB0AHQAdAB0AHQAdAB0AHQAdAB0AHQAdAB0AHQAdAE8ATwBPAE8ATwBPAE8ATwBPAE8ATwBPAE8ATwBPAE8ATwBPAE8ATwBPAFAAHQAdAB0AHQAdAB0AHQAdAB0AHQAdAB0AHgAeAB4AHgAdAB0AHQAdAB0AHQAdAB0AHQAdAB0AHQAdAB0AHQAdAB0AHQAdAB0AHQAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHQAdAB0AHQAdAB0AHQAdAB0AHQAdAB0AHQAdAB0AHQAeAB4AHQAdAB0AHQAeAB4AHgAeAB4AHgAeAB4AHgAeAB0AHQAeAB0AHQAdAB0AHQAdAB0AHgAeAB4AHgAeAB4AHgAeAB0AHQAeAB4AHQAdAB4AHgAeAB4AHQAdAB4AHgAeAB4AHQAdAB0AHgAeAB0AHgAeAB0AHQAdAB0AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAdAB0AHQAdAB4AHgAeAB4AHgAeAB4AHgAeAB0AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAlACUAJQAlAB4AHQAdAB4AHgAdAB4AHgAeAB4AHQAdAB4AHgAeAB4AJQAlAB0AHQAlAB4AJQAlACUAIAAlACUAHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAlACUAJQAeAB4AHgAeAB0AHgAdAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAdAB0AHgAdAB0AHQAeAB0AJQAdAB0AHgAdAB0AHgAdAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeACUAHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHQAdAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAlACUAJQAlACUAJQAlACUAJQAlACUAJQAdAB0AHQAdACUAHgAlACUAJQAdACUAJQAdAB0AHQAlACUAHQAdACUAHQAdACUAJQAlAB4AHQAeAB4AHgAeAB0AHQAlAB0AHQAdAB0AHQAdACUAJQAlACUAJQAdACUAJQAgACUAHQAdACUAJQAlACUAJQAlACUAJQAeAB4AHgAlACUAIAAgACAAIAAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB0AHgAeAB4AFwAXABcAFwAXABcAHgATABMAJQAeAB4AHgAWABEAFgARABYAEQAWABEAFgARABYAEQAWABEATwBPAE8ATwBPAE8ATwBPAE8ATwBPAE8ATwBPAE8ATwBPAE8ATwBPAE8ATwAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeABYAEQAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAWABEAFgARABYAEQAWABEAFgARAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AFgARABYAEQAWABEAFgARABYAEQAWABEAFgARABYAEQAWABEAFgARABYAEQAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAWABEAFgARAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AFgARAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAdAB0AHQAdAB0AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgArACsAHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AKwAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AUABQAFAAUAAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAEAAQABAAeAB4AKwArACsAKwArABMADQANAA0AUAATAA0AUABQAFAAUABQAFAAUABQACsAKwArACsAKwArACsAUAANACsAKwArACsAKwArACsAKwArACsAKwArACsAKwAEAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUAArACsAKwArACsAKwArACsAKwBQAFAAUABQAFAAUABQACsAUABQAFAAUABQAFAAUAArAFAAUABQAFAAUABQAFAAKwBQAFAAUABQAFAAUABQACsAFwAXABcAFwAXABcAFwAXABcAFwAXABcAFwAXAA0ADQANAA0ADQANAA0ADQAeAA0AFgANAB4AHgAXABcAHgAeABcAFwAWABEAFgARABYAEQAWABEADQANAA0ADQATAFAADQANAB4ADQANAB4AHgAeAB4AHgAMAAwADQANAA0AHgANAA0AFgANAA0ADQANAA0ADQANAA0AHgANAB4ADQANAB4AHgAeACsAKwArACsAKwArACsAKwArACsAKwArACsAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACsAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAKwArACsAKwArACsAKwArACsAKwArACsAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwAlACUAJQAlACUAJQAlACUAJQAlACUAJQArACsAKwArAA0AEQARACUAJQBHAFcAVwAWABEAFgARABYAEQAWABEAFgARACUAJQAWABEAFgARABYAEQAWABEAFQAWABEAEQAlAFcAVwBXAFcAVwBXAFcAVwBXAAQABAAEAAQABAAEACUAVwBXAFcAVwA2ACUAJQBXAFcAVwBHAEcAJQAlACUAKwBRAFcAUQBXAFEAVwBRAFcAUQBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFEAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBRAFcAUQBXAFEAVwBXAFcAVwBXAFcAUQBXAFcAVwBXAFcAVwBRAFEAKwArAAQABAAVABUARwBHAFcAFQBRAFcAUQBXAFEAVwBRAFcAUQBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFEAVwBRAFcAUQBXAFcAVwBXAFcAVwBRAFcAVwBXAFcAVwBXAFEAUQBXAFcAVwBXABUAUQBHAEcAVwArACsAKwArACsAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAKwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAKwAlACUAVwBXAFcAVwAlACUAJQAlACUAJQAlACUAJQAlACsAKwArACsAKwArACsAKwArACsAKwArAFEAUQBRAFEAUQBRAFEAUQBRAFEAUQBRAFEAUQBRAFEAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQArAFcAVwBXAFcAVwBXAFcAVwBXAFcAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQBPAE8ATwBPAE8ATwBPAE8AJQBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXACUAJQAlAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAEcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAKwArACsAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQArACsAKwArACsAKwArACsAKwBQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAADQATAA0AUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABLAEsASwBLAEsASwBLAEsASwBLAFAAUAArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAFAABAAEAAQABAAeAAQABAAEAAQABAAEAAQABAAEAAQAHgBQAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AUABQAAQABABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAAQABAAeAA0ADQANAA0ADQArACsAKwArACsAKwArACsAHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAFAAUABQAFAAUABQAFAAUABQAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AUAAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgBQAB4AHgAeAB4AHgAeAFAAHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgArACsAHgAeAB4AHgAeAB4AHgAeAB4AKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwAeAB4AUABQAFAAUABQAFAAUABQAFAAUABQAAQAUABQAFAABABQAFAAUABQAAQAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAAQABAAEAAQABAAeAB4AHgAeAAQAKwArACsAUABQAFAAUABQAFAAHgAeABoAHgArACsAKwArACsAKwBQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAADgAOABMAEwArACsAKwArACsAKwArACsABAAEAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAAQABAAEAAQABAAEACsAKwArACsAKwArACsAKwANAA0ASwBLAEsASwBLAEsASwBLAEsASwArACsAKwArACsAKwAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABABQAFAAUABQAFAAUAAeAB4AHgBQAA4AUABQAAQAUABQAFAAUABQAFAABAAEAAQABAAEAAQABAAEAA0ADQBQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQAKwArACsAKwArACsAKwArACsAKwArAB4AWABYAFgAWABYAFgAWABYAFgAWABYAFgAWABYAFgAWABYAFgAWABYAFgAWABYAFgAWABYAFgAWABYACsAKwArAAQAHgAeAB4AHgAeAB4ADQANAA0AHgAeAB4AHgArAFAASwBLAEsASwBLAEsASwBLAEsASwArACsAKwArAB4AHgBcAFwAXABcAFwAKgBcAFwAXABcAFwAXABcAFwAXABcAEsASwBLAEsASwBLAEsASwBLAEsAXABcAFwAXABcACsAUABQAFAAUABQAFAAUABQAFAABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEACsAKwArACsAKwArACsAKwArAFAAUABQAAQAUABQAFAAUABQAFAAUABQAAQABAArACsASwBLAEsASwBLAEsASwBLAEsASwArACsAHgANAA0ADQBcAFwAXABcAFwAXABcAFwAXABcAFwAXABcAFwAXABcAFwAXABcAFwAXABcAFwAKgAqACoAXAAqACoAKgBcAFwAXABcAFwAXABcAFwAXABcAFwAXABcAFwAXABcAFwAXAAqAFwAKgAqACoAXABcACoAKgBcAFwAXABcAFwAKgAqAFwAKgBcACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArAFwAXABcACoAKgBQAFAAUABQAFAAUABQAFAAUABQAFAABAAEAAQABAAEAA0ADQBQAFAAUAAEAAQAKwArACsAKwArACsAKwArACsAKwBQAFAAUABQAFAAUAArACsAUABQAFAAUABQAFAAKwArAFAAUABQAFAAUABQACsAKwArACsAKwArACsAKwArAFAAUABQAFAAUABQAFAAKwBQAFAAUABQAFAAUABQACsAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAHgAeACsAKwArACsAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUAAEAAQABAAEAAQABAAEAAQADQAEAAQAKwArAEsASwBLAEsASwBLAEsASwBLAEsAKwArACsAKwArACsAVABVAFUAVQBVAFUAVQBVAFUAVQBVAFUAVQBVAFUAVQBVAFUAVQBVAFUAVQBVAFUAVQBVAFUAVQBUAFUAVQBVAFUAVQBVAFUAVQBVAFUAVQBVAFUAVQBVAFUAVQBVAFUAVQBVAFUAVQBVAFUAVQBVACsAKwArACsAKwArACsAKwArACsAKwArAFkAWQBZAFkAWQBZAFkAWQBZAFkAWQBZAFkAWQBZAFkAWQBZAFkAKwArACsAKwBaAFoAWgBaAFoAWgBaAFoAWgBaAFoAWgBaAFoAWgBaAFoAWgBaAFoAWgBaAFoAWgBaAFoAWgBaAFoAKwArACsAKwAGAAYABgAGAAYABgAGAAYABgAGAAYABgAGAAYABgAGAAYABgAGAAYABgAGAAYABgAGAAYABgAGAAYABgAGAAYAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXACUAJQBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAJQAlACUAJQAlACUAUABQAFAAUABQAFAAUAArACsAKwArACsAKwArACsAKwArACsAKwBQAFAAUABQAFAAKwArACsAKwArAFYABABWAFYAVgBWAFYAVgBWAFYAVgBWAB4AVgBWAFYAVgBWAFYAVgBWAFYAVgBWAFYAVgArAFYAVgBWAFYAVgArAFYAKwBWAFYAKwBWAFYAKwBWAFYAVgBWAFYAVgBWAFYAVgBWAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAEQAWAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAKwArAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUAArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwBQAFAAUABQAFAAUABQAFAAUABQAFAAUAAaAB4AKwArAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQAGAARABEAGAAYABMAEwAWABEAFAArACsAKwArACsAKwAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEACUAJQAlACUAJQAWABEAFgARABYAEQAWABEAFgARABYAEQAlACUAFgARACUAJQAlACUAJQAlACUAEQAlABEAKwAVABUAEwATACUAFgARABYAEQAWABEAJQAlACUAJQAlACUAJQAlACsAJQAbABoAJQArACsAKwArAFAAUABQAFAAUAArAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAKwArAAcAKwATACUAJQAbABoAJQAlABYAEQAlACUAEQAlABEAJQBXAFcAVwBXAFcAVwBXAFcAVwBXABUAFQAlACUAJQATACUAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXABYAJQARACUAJQAlAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwAWACUAEQAlABYAEQARABYAEQARABUAVwBRAFEAUQBRAFEAUQBRAFEAUQBRAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAEcARwArACsAVwBXAFcAVwBXAFcAKwArAFcAVwBXAFcAVwBXACsAKwBXAFcAVwBXAFcAVwArACsAVwBXAFcAKwArACsAGgAbACUAJQAlABsAGwArAB4AHgAeAB4AHgAeAB4AKwArACsAKwArACsAKwArACsAKwAEAAQABAAQAB0AKwArAFAAUABQAFAAUABQAFAAUABQAFAAUABQACsAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUAArAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAKwBQAFAAKwBQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUAArACsAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQACsAKwBQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUAArACsAKwArACsADQANAA0AKwArACsAKwBQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQACsAKwArAB4AHgAeAB4AHgAeAB4AHgAeAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgBQAFAAHgAeAB4AKwAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAAQAKwArAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUAArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwAEAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQACsAKwArACsAKwArACsAKwArAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAKwArACsAKwArAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAABAAEAAQABAAEACsAKwArACsAKwBQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUAArAA0AUABQAFAAUAArACsAKwArAFAAUABQAFAAUABQAFAAUAANAFAAUABQAFAAUAArACsAKwArACsAKwArACsAKwArAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQACsAKwBQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAKwArACsAKwBQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQACsAKwArACsAKwArACsAKwBQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQACsAKwArACsAKwArACsAKwArACsAKwAeACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAUABQAFAAUABQAFAAKwArAFAAKwBQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUAArAFAAUAArACsAKwBQACsAKwBQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAKwANAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUAAeAB4AUABQAFAAUABQAFAAUAArACsAKwArACsAKwArAFAAUABQAFAAUABQAFAAUABQACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUAArAFAAUAArACsAKwArACsAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQACsAKwArAA0AUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQACsAKwArACsAKwAeAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQACsAKwArACsAUABQAFAAUABQAAQABAAEACsABAAEACsAKwArACsAKwAEAAQABAAEAFAAUABQAFAAKwBQAFAAUAArAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAKwArAAQABAAEACsAKwArACsABABQAFAAUABQAFAAUABQAFAAUAArACsAKwArACsAKwArAA0ADQANAA0ADQANAA0ADQAeACsAKwArACsAKwArACsAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUAAeAFAAUABQAFAAUABQAFAAUAAeAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAAQABAArACsAKwArAFAAUABQAFAAUAANAA0ADQANAA0ADQAUACsAKwArACsAKwArACsAKwArAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAKwArACsADQANAA0ADQANAA0ADQBQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUAArACsAKwArACsAKwArAB4AHgAeAB4AKwArACsAKwArACsAKwArACsAKwArACsAUABQAFAAUABQAFAAUAArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArAFAAUABQAFAAUABQAFAAUABQACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwBQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQACsAKwArACsAKwArACsAKwArACsAKwArACsAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUAArACsAKwArACsAKwArAFAAUABQAFAAUABQAAQABAAEAAQAKwArACsAKwArACsAKwArAEsASwBLAEsASwBLAEsASwBLAEsAKwArACsAKwArACsAUABQAFAAUABQAFAAUABQAFAAUAArAAQABAANACsAKwBQAFAAKwArACsAKwArACsAKwArACsAKwArACsAKwArAFAAUABQAFAAUABQAAQABAAEAAQABAAEAAQABAAEAAQABABQAFAAUABQAB4AHgAeAB4AHgArACsAKwArACsAKwAEAAQABAAEAAQABAAEAA0ADQAeAB4AHgAeAB4AKwArACsAKwBQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAEsASwBLAEsASwBLAEsASwBLAEsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsABABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAAQABAAEAAQABAAEAAQABAAEAAQABAAeAB4AHgANAA0ADQANACsAKwArACsAKwArACsAKwArACsAKwAeACsAKwBQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAKwArACsAKwArACsAKwBLAEsASwBLAEsASwBLAEsASwBLACsAKwArACsAKwArAFAAUABQAFAAUABQAFAABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEACsASwBLAEsASwBLAEsASwBLAEsASwANAA0ADQANAFAABAAEAFAAKwArACsAKwArACsAKwArAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAABAAeAA4AUAArACsAKwArACsAKwArACsAKwAEAFAAUABQAFAADQANAB4ADQAEAAQABAAEAB4ABAAEAEsASwBLAEsASwBLAEsASwBLAEsAUAAOAFAADQANAA0AKwBQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAKwArACsAKwArACsAKwArACsAKwArAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQACsAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUAAEAAQABAAEAAQABAAEAAQABAAEAAQABAANAA0AHgANAA0AHgAEACsAUABQAFAAUABQAFAAUAArAFAAKwBQAFAAUABQACsAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAKwBQAFAAUABQAFAAUABQAFAAUABQAA0AKwArACsAKwArACsAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUAAEAAQABAAEAAQABAAEAAQABAAEAAQAKwArACsAKwArAEsASwBLAEsASwBLAEsASwBLAEsAKwArACsAKwArACsABAAEAAQABAArAFAAUABQAFAAUABQAFAAUAArACsAUABQACsAKwBQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAKwBQAFAAUABQAFAAUABQACsAUABQACsAUABQAFAAUABQACsABAAEAFAABAAEAAQABAAEAAQABAArACsABAAEACsAKwAEAAQABAArACsAUAArACsAKwArACsAKwAEACsAKwArACsAKwBQAFAAUABQAFAABAAEACsAKwAEAAQABAAEAAQABAAEACsAKwArAAQABAAEAAQABAArACsAKwArACsAKwArACsAKwArACsABAAEAAQABAAEAAQABABQAFAAUABQAA0ADQANAA0AHgBLAEsASwBLAEsASwBLAEsASwBLAA0ADQArAB4ABABQAFAAUAArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwAEAAQABAAEAFAAUAAeAFAAKwArACsAKwArACsAKwArAEsASwBLAEsASwBLAEsASwBLAEsAKwArACsAKwArACsAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAABAAEAAQABAAEAAQABAArACsABAAEAAQABAAEAAQABAAEAAQADgANAA0AEwATAB4AHgAeAA0ADQANAA0ADQANAA0ADQANAA0ADQANAA0ADQANAFAAUABQAFAABAAEACsAKwAEAA0ADQAeAFAAKwArACsAKwArACsAKwArACsAKwArAEsASwBLAEsASwBLAEsASwBLAEsAKwArACsAKwArACsADgAOAA4ADgAOAA4ADgAOAA4ADgAOAA4ADgArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArAFAAUABQAFAAUABQAFAAUABQAFAAUAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAFAAKwArACsAKwArACsAKwBLAEsASwBLAEsASwBLAEsASwBLACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAXABcAFwAXABcAFwAXABcAFwAXABcAFwAXABcAFwAXABcAFwAXABcAFwAXABcAFwAXABcAFwAKwArACoAKgAqACoAKgAqACoAKgAqACoAKgAqACoAKgAqACsAKwArACsASwBLAEsASwBLAEsASwBLAEsASwBcAFwADQANAA0AKgBQAFAAUABQAFAAUABQAFAAUABQAFAAUAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAeACsAKwArACsASwBLAEsASwBLAEsASwBLAEsASwBQAFAAUABQAFAAUABQAFAAUAArACsAKwArACsAKwArACsAKwArACsAKwBQAFAAUABQAFAAUABQAFAAKwArAFAAKwArAFAAUABQAFAAUABQAFAAUAArAFAAUAArAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAABAAEAAQABAAEAAQAKwAEAAQAKwArAAQABAAEAAQAUAAEAFAABAAEAA0ADQANACsAKwArACsAKwArACsAKwArAEsASwBLAEsASwBLAEsASwBLAEsAKwArACsAKwArACsAUABQAFAAUABQAFAAUABQACsAKwBQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAABAAEAAQABAAEAAQABAArACsABAAEAAQABAAEAAQABABQAA4AUAAEACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArAFAABAAEAAQABAAEAAQABAAEAAQABABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUAAEAAQABAAEAAQABAAEAFAABAAEAAQABAAOAB4ADQANAA0ADQAOAB4ABAArACsAKwArACsAKwArACsAUAAEAAQABAAEAAQABAAEAAQABAAEAAQAUABQAFAAUABQAFAAUABQAFAAUAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAA0ADQANAFAADgAOAA4ADQANACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwBQAFAAUABQAFAAUABQAFAAUAArAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAABAAEAAQABAAEAAQABAAEACsABAAEAAQABAAEAAQABAAEAFAADQANAA0ADQANACsAKwArACsAKwArACsAKwArACsASwBLAEsASwBLAEsASwBLAEsASwBQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUAArACsAKwAOABMAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAKwArAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAArAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAArACsAKwArACsAKwArACsAKwBQAFAAUABQAFAAUABQACsAUABQACsAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUAAEAAQABAAEAAQABAArACsAKwAEACsABAAEACsABAAEAAQABAAEAAQABABQAAQAKwArACsAKwArACsAKwArAEsASwBLAEsASwBLAEsASwBLAEsAKwArACsAKwArACsAUABQAFAAUABQAFAAKwBQAFAAKwBQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUAAEAAQABAAEAAQAKwAEAAQAKwAEAAQABAAEAAQAUAArACsAKwArACsAKwArAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAABAAEAAQABAAeAB4AKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwBQACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAB4AHgAeAB4AHgAeAB4AHgAaABoAGgAaAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgArACsAKwArACsAKwArACsAKwArACsAKwArAA0AUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQACsAKwArACsAKwArAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQACsADQANAA0ADQANACsAKwArACsAKwArACsAKwArACsAKwBQAFAAUABQACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAASABIAEgAQwBDAEMAUABQAFAAUABDAFAAUABQAEgAQwBIAEMAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAASABDAEMAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAKwAJAAkACQAJAAkACQAJABYAEQArACsAKwArACsAKwArAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABIAEMAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUAArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArAEsASwBLAEsASwBLAEsASwBLAEsAKwArACsAKwANAA0AKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwBQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAKwArAAQABAAEAAQABAANACsAKwArACsAKwArACsAKwArACsAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUAAEAAQABAAEAAQABAAEAA0ADQANAB4AHgAeAB4AHgAeAFAAUABQAFAADQAeACsAKwArACsAKwArACsAKwArACsASwBLAEsASwBLAEsASwBLAEsASwArAFAAUABQAFAAUABQAFAAKwBQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUAArACsAKwArACsAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUAArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUAANAA0AHgAeACsAKwArACsAKwBQAFAAUABQAFAAUABQAFAAUABQAFAAKwArACsAKwAEAFAABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQAKwArACsAKwArACsAKwAEAAQABAAEAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAARwBHABUARwAJACsAKwArACsAKwArACsAKwArACsAKwAEAAQAKwArACsAKwArACsAKwArACsAKwArACsAKwArAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXACsAKwArACsAKwArACsAKwBXAFcAVwBXAFcAVwBXAFcAVwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAUQBRAFEAKwArACsAKwArACsAKwArACsAKwArACsAKwBRAFEAUQBRACsAKwArACsAKwArACsAKwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXACsAKwArACsAUABQAFAAUABQAFAAUABQAFAAUABQACsAKwArACsAKwBQAFAAUABQAFAAUABQAFAAUABQAFAAUABQACsAKwArACsAKwArACsAUABQAFAAUABQAFAAUABQAFAAUAArACsAHgAEAAQADQAEAAQABAAEACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgArACsAKwArACsAKwArACsAKwArAB4AHgAeAB4AHgAeAB4AKwArAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAAQABAAEAAQABAAeAB4AHgAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAB4AHgAEAAQABAAEAAQABAAEAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4ABAAEAAQABAAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4ABAAEAAQAHgArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQACsAKwArACsAKwArACsAKwArACsAKwArAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgArACsAKwArACsAKwArACsAKwAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgArAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AKwBQAFAAKwArAFAAKwArAFAAUAArACsAUABQAFAAUAArAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeACsAUAArAFAAUABQAFAAUABQAFAAKwAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AKwBQAFAAUABQACsAKwBQAFAAUABQAFAAUABQAFAAKwBQAFAAUABQAFAAUABQACsAHgAeAFAAUABQAFAAUAArAFAAKwArACsAUABQAFAAUABQAFAAUAArAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AKwArAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAHgBQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgBQAFAAUABQAFAAUABQAFAAUABQAFAAHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgBQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAB4AHgAeAB4AHgAeAB4AHgAeACsAKwBLAEsASwBLAEsASwBLAEsASwBLAEsASwBLAEsASwBLAEsASwBLAEsASwBLAEsASwBLAEsASwBLAEsASwBLAEsASwBLAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAeAB4AHgAeAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAeAB4AHgAeAB4AHgAeAB4ABAAeAB4AHgAeAB4AHgAeAB4AHgAeAAQAHgAeAA0ADQANAA0AHgArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwAEAAQABAAEAAQAKwAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArAAQABAAEAAQABAAEAAQAKwAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQAKwArAAQABAAEAAQABAAEAAQAKwAEAAQAKwAEAAQABAAEAAQAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUAArACsAKwAEAAQABAAEAAQABAAEAFAAUABQAFAAUABQAFAAKwArAEsASwBLAEsASwBLAEsASwBLAEsAKwArACsAKwBQAB4AKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwBQAFAAUABQAFAAUABQAFAAUABQAFAAUAAEAAQABAAEAEsASwBLAEsASwBLAEsASwBLAEsAKwArACsAKwArABsAUABQAFAAUABQACsAKwBQAFAAUABQAFAAUABQAFAAUAAEAAQABAAEAAQABAAEACsAKwArACsAKwArACsAKwArAB4AHgAeAB4ABAAEAAQABAAEAAQABABQACsAKwArACsASwBLAEsASwBLAEsASwBLAEsASwArACsAKwArABYAFgArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAGgBQAFAAUAAaAFAAUABQAFAAKwArACsAKwArACsAKwArACsAKwArAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUAAeAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQACsAKwBQAFAAUABQACsAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAKwBQAFAAKwBQACsAKwBQACsAUABQAFAAUABQAFAAUABQAFAAUAArAFAAUABQAFAAKwBQACsAUAArACsAKwArACsAKwBQACsAKwArACsAUAArAFAAKwBQACsAUABQAFAAKwBQAFAAKwBQACsAKwBQACsAUAArAFAAKwBQACsAUAArAFAAUAArAFAAKwArAFAAUABQAFAAKwBQAFAAUABQAFAAUABQACsAUABQAFAAUAArAFAAUABQAFAAKwBQACsAUABQAFAAUABQAFAAUABQAFAAUAArAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUAArACsAKwArACsAUABQAFAAKwBQAFAAUABQAFAAKwBQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwAeAB4AKwArACsAKwArACsAKwArACsAKwArACsAKwArAE8ATwBPAE8ATwBPAE8ATwBPAE8ATwBPAE8AJQAlACUAHQAdAB0AHQAdAB0AHQAdAB0AHQAdAB0AHQAdAB0AHQAdAB0AHgAeAB0AHQAdAB0AHQAdAB0AHQAdAB0AHQAdAB0AHQAdAB0AHQAdAB4AHgAeACUAJQAlAB0AHQAdAB0AHQAdAB0AHQAdAB0AHQAdAB0AHQAdAB0AHQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQApACkAKQApACkAKQApACkAKQApACkAKQApACkAKQApACkAKQApACkAKQApACkAKQApACkAJQAlACUAJQAlACAAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAeAB4AJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlAB4AHgAlACUAJQAlACUAHgAlACUAJQAlACUAIAAgACAAJQAlACAAJQAlACAAIAAgACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACEAIQAhACEAIQAlACUAIAAgACUAJQAgACAAIAAgACAAIAAgACAAIAAgACAAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAJQAlACUAIAAlACUAJQAlACAAIAAgACUAIAAgACAAJQAlACUAJQAlACUAJQAgACUAIAAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAHgAlAB4AJQAeACUAJQAlACUAJQAgACUAJQAlACUAHgAlAB4AHgAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlAB4AHgAeAB4AHgAeAB4AJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAeAB4AHgAeAB4AHgAeAB4AHgAeACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACAAIAAlACUAJQAlACAAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACAAJQAlACUAJQAgACAAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAHgAeAB4AHgAeAB4AHgAeACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAeAB4AHgAeAB4AHgAlACUAJQAlACUAJQAlACAAIAAgACUAJQAlACAAIAAgACAAIAAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeABcAFwAXABUAFQAVAB4AHgAeAB4AJQAlACUAIAAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACAAIAAgACUAJQAlACUAJQAlACUAJQAlACAAJQAlACUAJQAlACUAJQAlACUAJQAlACAAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AJQAlACUAJQAlACUAJQAlACUAJQAlACUAHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AJQAlACUAJQAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeACUAJQAlACUAJQAlACUAJQAeAB4AHgAeAB4AHgAeAB4AHgAeACUAJQAlACUAJQAlAB4AHgAeAB4AHgAeAB4AHgAlACUAJQAlACUAJQAlACUAHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAgACUAJQAgACUAJQAlACUAJQAlACUAJQAgACAAIAAgACAAIAAgACAAJQAlACUAJQAlACUAIAAlACUAJQAlACUAJQAlACUAJQAgACAAIAAgACAAIAAgACAAIAAgACUAJQAgACAAIAAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAgACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACAAIAAlACAAIAAlACAAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAgACAAIAAlACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAJQAlAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AKwAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArAEsASwBLAEsASwBLAEsASwBLAEsAKwArACsAKwArACsAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAKwArAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXACUAJQBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwAlACUAJQAlACUAJQAlACUAJQAlACUAVwBXACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAKwAEACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArAA=='; var LETTER_NUMBER_MODIFIER = 50; // Non-tailorable Line Breaking Classes var BK = 1; // Cause a line break (after) var CR$1 = 2; // Cause a line break (after), except between CR and LF var LF$1 = 3; // Cause a line break (after) var CM = 4; // Prohibit a line break between the character and the preceding character var NL = 5; // Cause a line break (after) var WJ = 7; // Prohibit line breaks before and after var ZW = 8; // Provide a break opportunity var GL = 9; // Prohibit line breaks before and after var SP = 10; // Enable indirect line breaks var ZWJ$1 = 11; // Prohibit line breaks within joiner sequences // Break Opportunities var B2 = 12; // Provide a line break opportunity before and after the character var BA = 13; // Generally provide a line break opportunity after the character var BB = 14; // Generally provide a line break opportunity before the character var HY = 15; // Provide a line break opportunity after the character, except in numeric context var CB = 16; // Provide a line break opportunity contingent on additional information // Characters Prohibiting Certain Breaks var CL = 17; // Prohibit line breaks before var CP = 18; // Prohibit line breaks before var EX = 19; // Prohibit line breaks before var IN = 20; // Allow only indirect line breaks between pairs var NS = 21; // Allow only indirect line breaks before var OP = 22; // Prohibit line breaks after var QU = 23; // Act like they are both opening and closing // Numeric Context var IS = 24; // Prevent breaks after any and before numeric var NU = 25; // Form numeric expressions for line breaking purposes var PO = 26; // Do not break following a numeric expression var PR = 27; // Do not break in front of a numeric expression var SY = 28; // Prevent a break before; and allow a break after // Other Characters var AI = 29; // Act like AL when the resolvedEAW is N; otherwise; act as ID var AL = 30; // Are alphabetic characters or symbols that are used with alphabetic characters var CJ = 31; // Treat as NS or ID for strict or normal breaking. var EB = 32; // Do not break from following Emoji Modifier var EM = 33; // Do not break from preceding Emoji Base var H2 = 34; // Form Korean syllable blocks var H3 = 35; // Form Korean syllable blocks var HL = 36; // Do not break around a following hyphen; otherwise act as Alphabetic var ID = 37; // Break before or after; except in some numeric context var JL = 38; // Form Korean syllable blocks var JV = 39; // Form Korean syllable blocks var JT = 40; // Form Korean syllable blocks var RI$1 = 41; // Keep pairs together. For pairs; break before and after other classes var SA = 42; // Provide a line break opportunity contingent on additional, language-specific context analysis var XX = 43; // Have as yet unknown line breaking behavior or unassigned code positions var ea_OP = [0x2329, 0xff08]; var BREAK_MANDATORY = '!'; var BREAK_NOT_ALLOWED$1 = '×'; var BREAK_ALLOWED$1 = '÷'; var UnicodeTrie$1 = createTrieFromBase64$1(base64$1); var ALPHABETICS = [AL, HL]; var HARD_LINE_BREAKS = [BK, CR$1, LF$1, NL]; var SPACE$1 = [SP, ZW]; var PREFIX_POSTFIX = [PR, PO]; var LINE_BREAKS = HARD_LINE_BREAKS.concat(SPACE$1); var KOREAN_SYLLABLE_BLOCK = [JL, JV, JT, H2, H3]; var HYPHEN = [HY, BA]; var codePointsToCharacterClasses = function (codePoints, lineBreak) { if (lineBreak === void 0) { lineBreak = 'strict'; } var types = []; var indices = []; var categories = []; codePoints.forEach(function (codePoint, index) { var classType = UnicodeTrie$1.get(codePoint); if (classType > LETTER_NUMBER_MODIFIER) { categories.push(true); classType -= LETTER_NUMBER_MODIFIER; } else { categories.push(false); } if (['normal', 'auto', 'loose'].indexOf(lineBreak) !== -1) { // U+2010, – U+2013, 〜 U+301C, ゠ U+30A0 if ([0x2010, 0x2013, 0x301c, 0x30a0].indexOf(codePoint) !== -1) { indices.push(index); return types.push(CB); } } if (classType === CM || classType === ZWJ$1) { // LB10 Treat any remaining combining mark or ZWJ as AL. if (index === 0) { indices.push(index); return types.push(AL); } // LB9 Do not break a combining character sequence; treat it as if it has the line breaking class of // the base character in all of the following rules. Treat ZWJ as if it were CM. var prev = types[index - 1]; if (LINE_BREAKS.indexOf(prev) === -1) { indices.push(indices[index - 1]); return types.push(prev); } indices.push(index); return types.push(AL); } indices.push(index); if (classType === CJ) { return types.push(lineBreak === 'strict' ? NS : ID); } if (classType === SA) { return types.push(AL); } if (classType === AI) { return types.push(AL); } // For supplementary characters, a useful default is to treat characters in the range 10000..1FFFD as AL // and characters in the ranges 20000..2FFFD and 30000..3FFFD as ID, until the implementation can be revised // to take into account the actual line breaking properties for these characters. if (classType === XX) { if ((codePoint >= 0x20000 && codePoint <= 0x2fffd) || (codePoint >= 0x30000 && codePoint <= 0x3fffd)) { return types.push(ID); } else { return types.push(AL); } } types.push(classType); }); return [indices, types, categories]; }; var isAdjacentWithSpaceIgnored = function (a, b, currentIndex, classTypes) { var current = classTypes[currentIndex]; if (Array.isArray(a) ? a.indexOf(current) !== -1 : a === current) { var i = currentIndex; while (i <= classTypes.length) { i++; var next = classTypes[i]; if (next === b) { return true; } if (next !== SP) { break; } } } if (current === SP) { var i = currentIndex; while (i > 0) { i--; var prev = classTypes[i]; if (Array.isArray(a) ? a.indexOf(prev) !== -1 : a === prev) { var n = currentIndex; while (n <= classTypes.length) { n++; var next = classTypes[n]; if (next === b) { return true; } if (next !== SP) { break; } } } if (prev !== SP) { break; } } } return false; }; var previousNonSpaceClassType = function (currentIndex, classTypes) { var i = currentIndex; while (i >= 0) { var type = classTypes[i]; if (type === SP) { i--; } else { return type; } } return 0; }; var _lineBreakAtIndex = function (codePoints, classTypes, indicies, index, forbiddenBreaks) { if (indicies[index] === 0) { return BREAK_NOT_ALLOWED$1; } var currentIndex = index - 1; if (Array.isArray(forbiddenBreaks) && forbiddenBreaks[currentIndex] === true) { return BREAK_NOT_ALLOWED$1; } var beforeIndex = currentIndex - 1; var afterIndex = currentIndex + 1; var current = classTypes[currentIndex]; // LB4 Always break after hard line breaks. // LB5 Treat CR followed by LF, as well as CR, LF, and NL as hard line breaks. var before = beforeIndex >= 0 ? classTypes[beforeIndex] : 0; var next = classTypes[afterIndex]; if (current === CR$1 && next === LF$1) { return BREAK_NOT_ALLOWED$1; } if (HARD_LINE_BREAKS.indexOf(current) !== -1) { return BREAK_MANDATORY; } // LB6 Do not break before hard line breaks. if (HARD_LINE_BREAKS.indexOf(next) !== -1) { return BREAK_NOT_ALLOWED$1; } // LB7 Do not break before spaces or zero width space. if (SPACE$1.indexOf(next) !== -1) { return BREAK_NOT_ALLOWED$1; } // LB8 Break before any character following a zero-width space, even if one or more spaces intervene. if (previousNonSpaceClassType(currentIndex, classTypes) === ZW) { return BREAK_ALLOWED$1; } // LB8a Do not break after a zero width joiner. if (UnicodeTrie$1.get(codePoints[currentIndex]) === ZWJ$1) { return BREAK_NOT_ALLOWED$1; } // zwj emojis if ((current === EB || current === EM) && UnicodeTrie$1.get(codePoints[afterIndex]) === ZWJ$1) { return BREAK_NOT_ALLOWED$1; } // LB11 Do not break before or after Word joiner and related characters. if (current === WJ || next === WJ) { return BREAK_NOT_ALLOWED$1; } // LB12 Do not break after NBSP and related characters. if (current === GL) { return BREAK_NOT_ALLOWED$1; } // LB12a Do not break before NBSP and related characters, except after spaces and hyphens. if ([SP, BA, HY].indexOf(current) === -1 && next === GL) { return BREAK_NOT_ALLOWED$1; } // LB13 Do not break before ‘]’ or ‘!’ or ‘;’ or ‘/’, even after spaces. if ([CL, CP, EX, IS, SY].indexOf(next) !== -1) { return BREAK_NOT_ALLOWED$1; } // LB14 Do not break after ‘[’, even after spaces. if (previousNonSpaceClassType(currentIndex, classTypes) === OP) { return BREAK_NOT_ALLOWED$1; } // LB15 Do not break within ‘”[’, even with intervening spaces. if (isAdjacentWithSpaceIgnored(QU, OP, currentIndex, classTypes)) { return BREAK_NOT_ALLOWED$1; } // LB16 Do not break between closing punctuation and a nonstarter (lb=NS), even with intervening spaces. if (isAdjacentWithSpaceIgnored([CL, CP], NS, currentIndex, classTypes)) { return BREAK_NOT_ALLOWED$1; } // LB17 Do not break within ‘——’, even with intervening spaces. if (isAdjacentWithSpaceIgnored(B2, B2, currentIndex, classTypes)) { return BREAK_NOT_ALLOWED$1; } // LB18 Break after spaces. if (current === SP) { return BREAK_ALLOWED$1; } // LB19 Do not break before or after quotation marks, such as ‘ ” ’. if (current === QU || next === QU) { return BREAK_NOT_ALLOWED$1; } // LB20 Break before and after unresolved CB. if (next === CB || current === CB) { return BREAK_ALLOWED$1; } // LB21 Do not break before hyphen-minus, other hyphens, fixed-width spaces, small kana, and other non-starters, or after acute accents. if ([BA, HY, NS].indexOf(next) !== -1 || current === BB) { return BREAK_NOT_ALLOWED$1; } // LB21a Don't break after Hebrew + Hyphen. if (before === HL && HYPHEN.indexOf(current) !== -1) { return BREAK_NOT_ALLOWED$1; } // LB21b Don’t break between Solidus and Hebrew letters. if (current === SY && next === HL) { return BREAK_NOT_ALLOWED$1; } // LB22 Do not break before ellipsis. if (next === IN) { return BREAK_NOT_ALLOWED$1; } // LB23 Do not break between digits and letters. if ((ALPHABETICS.indexOf(next) !== -1 && current === NU) || (ALPHABETICS.indexOf(current) !== -1 && next === NU)) { return BREAK_NOT_ALLOWED$1; } // LB23a Do not break between numeric prefixes and ideographs, or between ideographs and numeric postfixes. if ((current === PR && [ID, EB, EM].indexOf(next) !== -1) || ([ID, EB, EM].indexOf(current) !== -1 && next === PO)) { return BREAK_NOT_ALLOWED$1; } // LB24 Do not break between numeric prefix/postfix and letters, or between letters and prefix/postfix. if ((ALPHABETICS.indexOf(current) !== -1 && PREFIX_POSTFIX.indexOf(next) !== -1) || (PREFIX_POSTFIX.indexOf(current) !== -1 && ALPHABETICS.indexOf(next) !== -1)) { return BREAK_NOT_ALLOWED$1; } // LB25 Do not break between the following pairs of classes relevant to numbers: if ( // (PR | PO) × ( OP | HY )? NU ([PR, PO].indexOf(current) !== -1 && (next === NU || ([OP, HY].indexOf(next) !== -1 && classTypes[afterIndex + 1] === NU))) || // ( OP | HY ) × NU ([OP, HY].indexOf(current) !== -1 && next === NU) || // NU × (NU | SY | IS) (current === NU && [NU, SY, IS].indexOf(next) !== -1)) { return BREAK_NOT_ALLOWED$1; } // NU (NU | SY | IS)* × (NU | SY | IS | CL | CP) if ([NU, SY, IS, CL, CP].indexOf(next) !== -1) { var prevIndex = currentIndex; while (prevIndex >= 0) { var type = classTypes[prevIndex]; if (type === NU) { return BREAK_NOT_ALLOWED$1; } else if ([SY, IS].indexOf(type) !== -1) { prevIndex--; } else { break; } } } // NU (NU | SY | IS)* (CL | CP)? × (PO | PR)) if ([PR, PO].indexOf(next) !== -1) { var prevIndex = [CL, CP].indexOf(current) !== -1 ? beforeIndex : currentIndex; while (prevIndex >= 0) { var type = classTypes[prevIndex]; if (type === NU) { return BREAK_NOT_ALLOWED$1; } else if ([SY, IS].indexOf(type) !== -1) { prevIndex--; } else { break; } } } // LB26 Do not break a Korean syllable. if ((JL === current && [JL, JV, H2, H3].indexOf(next) !== -1) || ([JV, H2].indexOf(current) !== -1 && [JV, JT].indexOf(next) !== -1) || ([JT, H3].indexOf(current) !== -1 && next === JT)) { return BREAK_NOT_ALLOWED$1; } // LB27 Treat a Korean Syllable Block the same as ID. if ((KOREAN_SYLLABLE_BLOCK.indexOf(current) !== -1 && [IN, PO].indexOf(next) !== -1) || (KOREAN_SYLLABLE_BLOCK.indexOf(next) !== -1 && current === PR)) { return BREAK_NOT_ALLOWED$1; } // LB28 Do not break between alphabetics (“at”). if (ALPHABETICS.indexOf(current) !== -1 && ALPHABETICS.indexOf(next) !== -1) { return BREAK_NOT_ALLOWED$1; } // LB29 Do not break between numeric punctuation and alphabetics (“e.g.”). if (current === IS && ALPHABETICS.indexOf(next) !== -1) { return BREAK_NOT_ALLOWED$1; } // LB30 Do not break between letters, numbers, or ordinary symbols and opening or closing parentheses. if ((ALPHABETICS.concat(NU).indexOf(current) !== -1 && next === OP && ea_OP.indexOf(codePoints[afterIndex]) === -1) || (ALPHABETICS.concat(NU).indexOf(next) !== -1 && current === CP)) { return BREAK_NOT_ALLOWED$1; } // LB30a Break between two regional indicator symbols if and only if there are an even number of regional // indicators preceding the position of the break. if (current === RI$1 && next === RI$1) { var i = indicies[currentIndex]; var count = 1; while (i > 0) { i--; if (classTypes[i] === RI$1) { count++; } else { break; } } if (count % 2 !== 0) { return BREAK_NOT_ALLOWED$1; } } // LB30b Do not break between an emoji base and an emoji modifier. if (current === EB && next === EM) { return BREAK_NOT_ALLOWED$1; } return BREAK_ALLOWED$1; }; var cssFormattedClasses = function (codePoints, options) { if (!options) { options = { lineBreak: 'normal', wordBreak: 'normal' }; } var _a = codePointsToCharacterClasses(codePoints, options.lineBreak), indicies = _a[0], classTypes = _a[1], isLetterNumber = _a[2]; if (options.wordBreak === 'break-all' || options.wordBreak === 'break-word') { classTypes = classTypes.map(function (type) { return ([NU, AL, SA].indexOf(type) !== -1 ? ID : type); }); } var forbiddenBreakpoints = options.wordBreak === 'keep-all' ? isLetterNumber.map(function (letterNumber, i) { return letterNumber && codePoints[i] >= 0x4e00 && codePoints[i] <= 0x9fff; }) : undefined; return [indicies, classTypes, forbiddenBreakpoints]; }; var Break = /** @class */ (function () { function Break(codePoints, lineBreak, start, end) { this.codePoints = codePoints; this.required = lineBreak === BREAK_MANDATORY; this.start = start; this.end = end; } Break.prototype.slice = function () { return fromCodePoint$1.apply(void 0, this.codePoints.slice(this.start, this.end)); }; return Break; }()); var LineBreaker = function (str, options) { var codePoints = toCodePoints$1(str); var _a = cssFormattedClasses(codePoints, options), indicies = _a[0], classTypes = _a[1], forbiddenBreakpoints = _a[2]; var length = codePoints.length; var lastEnd = 0; var nextIndex = 0; return { next: function () { if (nextIndex >= length) { return { done: true, value: null }; } var lineBreak = BREAK_NOT_ALLOWED$1; while (nextIndex < length && (lineBreak = _lineBreakAtIndex(codePoints, classTypes, indicies, ++nextIndex, forbiddenBreakpoints)) === BREAK_NOT_ALLOWED$1) { } if (lineBreak !== BREAK_NOT_ALLOWED$1 || nextIndex === length) { var value = new Break(codePoints, lineBreak, lastEnd, nextIndex); lastEnd = nextIndex; return { value: value, done: false }; } return { done: true, value: null }; }, }; }; // https://www.w3.org/TR/css-syntax-3 var FLAG_UNRESTRICTED = 1 << 0; var FLAG_ID = 1 << 1; var FLAG_INTEGER = 1 << 2; var FLAG_NUMBER = 1 << 3; var LINE_FEED = 0x000a; var SOLIDUS = 0x002f; var REVERSE_SOLIDUS = 0x005c; var CHARACTER_TABULATION = 0x0009; var SPACE = 0x0020; var QUOTATION_MARK = 0x0022; var EQUALS_SIGN = 0x003d; var NUMBER_SIGN = 0x0023; var DOLLAR_SIGN = 0x0024; var PERCENTAGE_SIGN = 0x0025; var APOSTROPHE = 0x0027; var LEFT_PARENTHESIS = 0x0028; var RIGHT_PARENTHESIS = 0x0029; var LOW_LINE = 0x005f; var HYPHEN_MINUS = 0x002d; var EXCLAMATION_MARK = 0x0021; var LESS_THAN_SIGN = 0x003c; var GREATER_THAN_SIGN = 0x003e; var COMMERCIAL_AT = 0x0040; var LEFT_SQUARE_BRACKET = 0x005b; var RIGHT_SQUARE_BRACKET = 0x005d; var CIRCUMFLEX_ACCENT = 0x003d; var LEFT_CURLY_BRACKET = 0x007b; var QUESTION_MARK = 0x003f; var RIGHT_CURLY_BRACKET = 0x007d; var VERTICAL_LINE = 0x007c; var TILDE = 0x007e; var CONTROL = 0x0080; var REPLACEMENT_CHARACTER = 0xfffd; var ASTERISK = 0x002a; var PLUS_SIGN = 0x002b; var COMMA = 0x002c; var COLON = 0x003a; var SEMICOLON = 0x003b; var FULL_STOP = 0x002e; var NULL = 0x0000; var BACKSPACE = 0x0008; var LINE_TABULATION = 0x000b; var SHIFT_OUT = 0x000e; var INFORMATION_SEPARATOR_ONE = 0x001f; var DELETE = 0x007f; var EOF = -1; var ZERO$1 = 0x0030; var a = 0x0061; var e = 0x0065; var f = 0x0066; var u = 0x0075; var z = 0x007a; var A = 0x0041; var E = 0x0045; var F = 0x0046; var U = 0x0055; var Z = 0x005a; var isDigit = function (codePoint) { return codePoint >= ZERO$1 && codePoint <= 0x0039; }; var isSurrogateCodePoint = function (codePoint) { return codePoint >= 0xd800 && codePoint <= 0xdfff; }; var isHex = function (codePoint) { return isDigit(codePoint) || (codePoint >= A && codePoint <= F) || (codePoint >= a && codePoint <= f); }; var isLowerCaseLetter = function (codePoint) { return codePoint >= a && codePoint <= z; }; var isUpperCaseLetter = function (codePoint) { return codePoint >= A && codePoint <= Z; }; var isLetter = function (codePoint) { return isLowerCaseLetter(codePoint) || isUpperCaseLetter(codePoint); }; var isNonASCIICodePoint = function (codePoint) { return codePoint >= CONTROL; }; var isWhiteSpace = function (codePoint) { return codePoint === LINE_FEED || codePoint === CHARACTER_TABULATION || codePoint === SPACE; }; var isNameStartCodePoint = function (codePoint) { return isLetter(codePoint) || isNonASCIICodePoint(codePoint) || codePoint === LOW_LINE; }; var isNameCodePoint = function (codePoint) { return isNameStartCodePoint(codePoint) || isDigit(codePoint) || codePoint === HYPHEN_MINUS; }; var isNonPrintableCodePoint = function (codePoint) { return ((codePoint >= NULL && codePoint <= BACKSPACE) || codePoint === LINE_TABULATION || (codePoint >= SHIFT_OUT && codePoint <= INFORMATION_SEPARATOR_ONE) || codePoint === DELETE); }; var isValidEscape = function (c1, c2) { if (c1 !== REVERSE_SOLIDUS) { return false; } return c2 !== LINE_FEED; }; var isIdentifierStart = function (c1, c2, c3) { if (c1 === HYPHEN_MINUS) { return isNameStartCodePoint(c2) || isValidEscape(c2, c3); } else if (isNameStartCodePoint(c1)) { return true; } else if (c1 === REVERSE_SOLIDUS && isValidEscape(c1, c2)) { return true; } return false; }; var isNumberStart = function (c1, c2, c3) { if (c1 === PLUS_SIGN || c1 === HYPHEN_MINUS) { if (isDigit(c2)) { return true; } return c2 === FULL_STOP && isDigit(c3); } if (c1 === FULL_STOP) { return isDigit(c2); } return isDigit(c1); }; var stringToNumber = function (codePoints) { var c = 0; var sign = 1; if (codePoints[c] === PLUS_SIGN || codePoints[c] === HYPHEN_MINUS) { if (codePoints[c] === HYPHEN_MINUS) { sign = -1; } c++; } var integers = []; while (isDigit(codePoints[c])) { integers.push(codePoints[c++]); } var int = integers.length ? parseInt(fromCodePoint$1.apply(void 0, integers), 10) : 0; if (codePoints[c] === FULL_STOP) { c++; } var fraction = []; while (isDigit(codePoints[c])) { fraction.push(codePoints[c++]); } var fracd = fraction.length; var frac = fracd ? parseInt(fromCodePoint$1.apply(void 0, fraction), 10) : 0; if (codePoints[c] === E || codePoints[c] === e) { c++; } var expsign = 1; if (codePoints[c] === PLUS_SIGN || codePoints[c] === HYPHEN_MINUS) { if (codePoints[c] === HYPHEN_MINUS) { expsign = -1; } c++; } var exponent = []; while (isDigit(codePoints[c])) { exponent.push(codePoints[c++]); } var exp = exponent.length ? parseInt(fromCodePoint$1.apply(void 0, exponent), 10) : 0; return sign * (int + frac * Math.pow(10, -fracd)) * Math.pow(10, expsign * exp); }; var LEFT_PARENTHESIS_TOKEN = { type: 2 /* LEFT_PARENTHESIS_TOKEN */ }; var RIGHT_PARENTHESIS_TOKEN = { type: 3 /* RIGHT_PARENTHESIS_TOKEN */ }; var COMMA_TOKEN = { type: 4 /* COMMA_TOKEN */ }; var SUFFIX_MATCH_TOKEN = { type: 13 /* SUFFIX_MATCH_TOKEN */ }; var PREFIX_MATCH_TOKEN = { type: 8 /* PREFIX_MATCH_TOKEN */ }; var COLUMN_TOKEN = { type: 21 /* COLUMN_TOKEN */ }; var DASH_MATCH_TOKEN = { type: 9 /* DASH_MATCH_TOKEN */ }; var INCLUDE_MATCH_TOKEN = { type: 10 /* INCLUDE_MATCH_TOKEN */ }; var LEFT_CURLY_BRACKET_TOKEN = { type: 11 /* LEFT_CURLY_BRACKET_TOKEN */ }; var RIGHT_CURLY_BRACKET_TOKEN = { type: 12 /* RIGHT_CURLY_BRACKET_TOKEN */ }; var SUBSTRING_MATCH_TOKEN = { type: 14 /* SUBSTRING_MATCH_TOKEN */ }; var BAD_URL_TOKEN = { type: 23 /* BAD_URL_TOKEN */ }; var BAD_STRING_TOKEN = { type: 1 /* BAD_STRING_TOKEN */ }; var CDO_TOKEN = { type: 25 /* CDO_TOKEN */ }; var CDC_TOKEN = { type: 24 /* CDC_TOKEN */ }; var COLON_TOKEN = { type: 26 /* COLON_TOKEN */ }; var SEMICOLON_TOKEN = { type: 27 /* SEMICOLON_TOKEN */ }; var LEFT_SQUARE_BRACKET_TOKEN = { type: 28 /* LEFT_SQUARE_BRACKET_TOKEN */ }; var RIGHT_SQUARE_BRACKET_TOKEN = { type: 29 /* RIGHT_SQUARE_BRACKET_TOKEN */ }; var WHITESPACE_TOKEN = { type: 31 /* WHITESPACE_TOKEN */ }; var EOF_TOKEN = { type: 32 /* EOF_TOKEN */ }; var Tokenizer = /** @class */ (function () { function Tokenizer() { this._value = []; } Tokenizer.prototype.write = function (chunk) { this._value = this._value.concat(toCodePoints$1(chunk)); }; Tokenizer.prototype.read = function () { var tokens = []; var token = this.consumeToken(); while (token !== EOF_TOKEN) { tokens.push(token); token = this.consumeToken(); } return tokens; }; Tokenizer.prototype.consumeToken = function () { var codePoint = this.consumeCodePoint(); switch (codePoint) { case QUOTATION_MARK: return this.consumeStringToken(QUOTATION_MARK); case NUMBER_SIGN: var c1 = this.peekCodePoint(0); var c2 = this.peekCodePoint(1); var c3 = this.peekCodePoint(2); if (isNameCodePoint(c1) || isValidEscape(c2, c3)) { var flags = isIdentifierStart(c1, c2, c3) ? FLAG_ID : FLAG_UNRESTRICTED; var value = this.consumeName(); return { type: 5 /* HASH_TOKEN */, value: value, flags: flags }; } break; case DOLLAR_SIGN: if (this.peekCodePoint(0) === EQUALS_SIGN) { this.consumeCodePoint(); return SUFFIX_MATCH_TOKEN; } break; case APOSTROPHE: return this.consumeStringToken(APOSTROPHE); case LEFT_PARENTHESIS: return LEFT_PARENTHESIS_TOKEN; case RIGHT_PARENTHESIS: return RIGHT_PARENTHESIS_TOKEN; case ASTERISK: if (this.peekCodePoint(0) === EQUALS_SIGN) { this.consumeCodePoint(); return SUBSTRING_MATCH_TOKEN; } break; case PLUS_SIGN: if (isNumberStart(codePoint, this.peekCodePoint(0), this.peekCodePoint(1))) { this.reconsumeCodePoint(codePoint); return this.consumeNumericToken(); } break; case COMMA: return COMMA_TOKEN; case HYPHEN_MINUS: var e1 = codePoint; var e2 = this.peekCodePoint(0); var e3 = this.peekCodePoint(1); if (isNumberStart(e1, e2, e3)) { this.reconsumeCodePoint(codePoint); return this.consumeNumericToken(); } if (isIdentifierStart(e1, e2, e3)) { this.reconsumeCodePoint(codePoint); return this.consumeIdentLikeToken(); } if (e2 === HYPHEN_MINUS && e3 === GREATER_THAN_SIGN) { this.consumeCodePoint(); this.consumeCodePoint(); return CDC_TOKEN; } break; case FULL_STOP: if (isNumberStart(codePoint, this.peekCodePoint(0), this.peekCodePoint(1))) { this.reconsumeCodePoint(codePoint); return this.consumeNumericToken(); } break; case SOLIDUS: if (this.peekCodePoint(0) === ASTERISK) { this.consumeCodePoint(); while (true) { var c = this.consumeCodePoint(); if (c === ASTERISK) { c = this.consumeCodePoint(); if (c === SOLIDUS) { return this.consumeToken(); } } if (c === EOF) { return this.consumeToken(); } } } break; case COLON: return COLON_TOKEN; case SEMICOLON: return SEMICOLON_TOKEN; case LESS_THAN_SIGN: if (this.peekCodePoint(0) === EXCLAMATION_MARK && this.peekCodePoint(1) === HYPHEN_MINUS && this.peekCodePoint(2) === HYPHEN_MINUS) { this.consumeCodePoint(); this.consumeCodePoint(); return CDO_TOKEN; } break; case COMMERCIAL_AT: var a1 = this.peekCodePoint(0); var a2 = this.peekCodePoint(1); var a3 = this.peekCodePoint(2); if (isIdentifierStart(a1, a2, a3)) { var value = this.consumeName(); return { type: 7 /* AT_KEYWORD_TOKEN */, value: value }; } break; case LEFT_SQUARE_BRACKET: return LEFT_SQUARE_BRACKET_TOKEN; case REVERSE_SOLIDUS: if (isValidEscape(codePoint, this.peekCodePoint(0))) { this.reconsumeCodePoint(codePoint); return this.consumeIdentLikeToken(); } break; case RIGHT_SQUARE_BRACKET: return RIGHT_SQUARE_BRACKET_TOKEN; case CIRCUMFLEX_ACCENT: if (this.peekCodePoint(0) === EQUALS_SIGN) { this.consumeCodePoint(); return PREFIX_MATCH_TOKEN; } break; case LEFT_CURLY_BRACKET: return LEFT_CURLY_BRACKET_TOKEN; case RIGHT_CURLY_BRACKET: return RIGHT_CURLY_BRACKET_TOKEN; case u: case U: var u1 = this.peekCodePoint(0); var u2 = this.peekCodePoint(1); if (u1 === PLUS_SIGN && (isHex(u2) || u2 === QUESTION_MARK)) { this.consumeCodePoint(); this.consumeUnicodeRangeToken(); } this.reconsumeCodePoint(codePoint); return this.consumeIdentLikeToken(); case VERTICAL_LINE: if (this.peekCodePoint(0) === EQUALS_SIGN) { this.consumeCodePoint(); return DASH_MATCH_TOKEN; } if (this.peekCodePoint(0) === VERTICAL_LINE) { this.consumeCodePoint(); return COLUMN_TOKEN; } break; case TILDE: if (this.peekCodePoint(0) === EQUALS_SIGN) { this.consumeCodePoint(); return INCLUDE_MATCH_TOKEN; } break; case EOF: return EOF_TOKEN; } if (isWhiteSpace(codePoint)) { this.consumeWhiteSpace(); return WHITESPACE_TOKEN; } if (isDigit(codePoint)) { this.reconsumeCodePoint(codePoint); return this.consumeNumericToken(); } if (isNameStartCodePoint(codePoint)) { this.reconsumeCodePoint(codePoint); return this.consumeIdentLikeToken(); } return { type: 6 /* DELIM_TOKEN */, value: fromCodePoint$1(codePoint) }; }; Tokenizer.prototype.consumeCodePoint = function () { var value = this._value.shift(); return typeof value === 'undefined' ? -1 : value; }; Tokenizer.prototype.reconsumeCodePoint = function (codePoint) { this._value.unshift(codePoint); }; Tokenizer.prototype.peekCodePoint = function (delta) { if (delta >= this._value.length) { return -1; } return this._value[delta]; }; Tokenizer.prototype.consumeUnicodeRangeToken = function () { var digits = []; var codePoint = this.consumeCodePoint(); while (isHex(codePoint) && digits.length < 6) { digits.push(codePoint); codePoint = this.consumeCodePoint(); } var questionMarks = false; while (codePoint === QUESTION_MARK && digits.length < 6) { digits.push(codePoint); codePoint = this.consumeCodePoint(); questionMarks = true; } if (questionMarks) { var start_1 = parseInt(fromCodePoint$1.apply(void 0, digits.map(function (digit) { return (digit === QUESTION_MARK ? ZERO$1 : digit); })), 16); var end = parseInt(fromCodePoint$1.apply(void 0, digits.map(function (digit) { return (digit === QUESTION_MARK ? F : digit); })), 16); return { type: 30 /* UNICODE_RANGE_TOKEN */, start: start_1, end: end }; } var start = parseInt(fromCodePoint$1.apply(void 0, digits), 16); if (this.peekCodePoint(0) === HYPHEN_MINUS && isHex(this.peekCodePoint(1))) { this.consumeCodePoint(); codePoint = this.consumeCodePoint(); var endDigits = []; while (isHex(codePoint) && endDigits.length < 6) { endDigits.push(codePoint); codePoint = this.consumeCodePoint(); } var end = parseInt(fromCodePoint$1.apply(void 0, endDigits), 16); return { type: 30 /* UNICODE_RANGE_TOKEN */, start: start, end: end }; } else { return { type: 30 /* UNICODE_RANGE_TOKEN */, start: start, end: start }; } }; Tokenizer.prototype.consumeIdentLikeToken = function () { var value = this.consumeName(); if (value.toLowerCase() === 'url' && this.peekCodePoint(0) === LEFT_PARENTHESIS) { this.consumeCodePoint(); return this.consumeUrlToken(); } else if (this.peekCodePoint(0) === LEFT_PARENTHESIS) { this.consumeCodePoint(); return { type: 19 /* FUNCTION_TOKEN */, value: value }; } return { type: 20 /* IDENT_TOKEN */, value: value }; }; Tokenizer.prototype.consumeUrlToken = function () { var value = []; this.consumeWhiteSpace(); if (this.peekCodePoint(0) === EOF) { return { type: 22 /* URL_TOKEN */, value: '' }; } var next = this.peekCodePoint(0); if (next === APOSTROPHE || next === QUOTATION_MARK) { var stringToken = this.consumeStringToken(this.consumeCodePoint()); if (stringToken.type === 0 /* STRING_TOKEN */) { this.consumeWhiteSpace(); if (this.peekCodePoint(0) === EOF || this.peekCodePoint(0) === RIGHT_PARENTHESIS) { this.consumeCodePoint(); return { type: 22 /* URL_TOKEN */, value: stringToken.value }; } } this.consumeBadUrlRemnants(); return BAD_URL_TOKEN; } while (true) { var codePoint = this.consumeCodePoint(); if (codePoint === EOF || codePoint === RIGHT_PARENTHESIS) { return { type: 22 /* URL_TOKEN */, value: fromCodePoint$1.apply(void 0, value) }; } else if (isWhiteSpace(codePoint)) { this.consumeWhiteSpace(); if (this.peekCodePoint(0) === EOF || this.peekCodePoint(0) === RIGHT_PARENTHESIS) { this.consumeCodePoint(); return { type: 22 /* URL_TOKEN */, value: fromCodePoint$1.apply(void 0, value) }; } this.consumeBadUrlRemnants(); return BAD_URL_TOKEN; } else if (codePoint === QUOTATION_MARK || codePoint === APOSTROPHE || codePoint === LEFT_PARENTHESIS || isNonPrintableCodePoint(codePoint)) { this.consumeBadUrlRemnants(); return BAD_URL_TOKEN; } else if (codePoint === REVERSE_SOLIDUS) { if (isValidEscape(codePoint, this.peekCodePoint(0))) { value.push(this.consumeEscapedCodePoint()); } else { this.consumeBadUrlRemnants(); return BAD_URL_TOKEN; } } else { value.push(codePoint); } } }; Tokenizer.prototype.consumeWhiteSpace = function () { while (isWhiteSpace(this.peekCodePoint(0))) { this.consumeCodePoint(); } }; Tokenizer.prototype.consumeBadUrlRemnants = function () { while (true) { var codePoint = this.consumeCodePoint(); if (codePoint === RIGHT_PARENTHESIS || codePoint === EOF) { return; } if (isValidEscape(codePoint, this.peekCodePoint(0))) { this.consumeEscapedCodePoint(); } } }; Tokenizer.prototype.consumeStringSlice = function (count) { var SLICE_STACK_SIZE = 50000; var value = ''; while (count > 0) { var amount = Math.min(SLICE_STACK_SIZE, count); value += fromCodePoint$1.apply(void 0, this._value.splice(0, amount)); count -= amount; } this._value.shift(); return value; }; Tokenizer.prototype.consumeStringToken = function (endingCodePoint) { var value = ''; var i = 0; do { var codePoint = this._value[i]; if (codePoint === EOF || codePoint === undefined || codePoint === endingCodePoint) { value += this.consumeStringSlice(i); return { type: 0 /* STRING_TOKEN */, value: value }; } if (codePoint === LINE_FEED) { this._value.splice(0, i); return BAD_STRING_TOKEN; } if (codePoint === REVERSE_SOLIDUS) { var next = this._value[i + 1]; if (next !== EOF && next !== undefined) { if (next === LINE_FEED) { value += this.consumeStringSlice(i); i = -1; this._value.shift(); } else if (isValidEscape(codePoint, next)) { value += this.consumeStringSlice(i); value += fromCodePoint$1(this.consumeEscapedCodePoint()); i = -1; } } } i++; } while (true); }; Tokenizer.prototype.consumeNumber = function () { var repr = []; var type = FLAG_INTEGER; var c1 = this.peekCodePoint(0); if (c1 === PLUS_SIGN || c1 === HYPHEN_MINUS) { repr.push(this.consumeCodePoint()); } while (isDigit(this.peekCodePoint(0))) { repr.push(this.consumeCodePoint()); } c1 = this.peekCodePoint(0); var c2 = this.peekCodePoint(1); if (c1 === FULL_STOP && isDigit(c2)) { repr.push(this.consumeCodePoint(), this.consumeCodePoint()); type = FLAG_NUMBER; while (isDigit(this.peekCodePoint(0))) { repr.push(this.consumeCodePoint()); } } c1 = this.peekCodePoint(0); c2 = this.peekCodePoint(1); var c3 = this.peekCodePoint(2); if ((c1 === E || c1 === e) && (((c2 === PLUS_SIGN || c2 === HYPHEN_MINUS) && isDigit(c3)) || isDigit(c2))) { repr.push(this.consumeCodePoint(), this.consumeCodePoint()); type = FLAG_NUMBER; while (isDigit(this.peekCodePoint(0))) { repr.push(this.consumeCodePoint()); } } return [stringToNumber(repr), type]; }; Tokenizer.prototype.consumeNumericToken = function () { var _a = this.consumeNumber(), number = _a[0], flags = _a[1]; var c1 = this.peekCodePoint(0); var c2 = this.peekCodePoint(1); var c3 = this.peekCodePoint(2); if (isIdentifierStart(c1, c2, c3)) { var unit = this.consumeName(); return { type: 15 /* DIMENSION_TOKEN */, number: number, flags: flags, unit: unit }; } if (c1 === PERCENTAGE_SIGN) { this.consumeCodePoint(); return { type: 16 /* PERCENTAGE_TOKEN */, number: number, flags: flags }; } return { type: 17 /* NUMBER_TOKEN */, number: number, flags: flags }; }; Tokenizer.prototype.consumeEscapedCodePoint = function () { var codePoint = this.consumeCodePoint(); if (isHex(codePoint)) { var hex = fromCodePoint$1(codePoint); while (isHex(this.peekCodePoint(0)) && hex.length < 6) { hex += fromCodePoint$1(this.consumeCodePoint()); } if (isWhiteSpace(this.peekCodePoint(0))) { this.consumeCodePoint(); } var hexCodePoint = parseInt(hex, 16); if (hexCodePoint === 0 || isSurrogateCodePoint(hexCodePoint) || hexCodePoint > 0x10ffff) { return REPLACEMENT_CHARACTER; } return hexCodePoint; } if (codePoint === EOF) { return REPLACEMENT_CHARACTER; } return codePoint; }; Tokenizer.prototype.consumeName = function () { var result = ''; while (true) { var codePoint = this.consumeCodePoint(); if (isNameCodePoint(codePoint)) { result += fromCodePoint$1(codePoint); } else if (isValidEscape(codePoint, this.peekCodePoint(0))) { result += fromCodePoint$1(this.consumeEscapedCodePoint()); } else { this.reconsumeCodePoint(codePoint); return result; } } }; return Tokenizer; }()); var Parser = /** @class */ (function () { function Parser(tokens) { this._tokens = tokens; } Parser.create = function (value) { var tokenizer = new Tokenizer(); tokenizer.write(value); return new Parser(tokenizer.read()); }; Parser.parseValue = function (value) { return Parser.create(value).parseComponentValue(); }; Parser.parseValues = function (value) { return Parser.create(value).parseComponentValues(); }; Parser.prototype.parseComponentValue = function () { var token = this.consumeToken(); while (token.type === 31 /* WHITESPACE_TOKEN */) { token = this.consumeToken(); } if (token.type === 32 /* EOF_TOKEN */) { throw new SyntaxError("Error parsing CSS component value, unexpected EOF"); } this.reconsumeToken(token); var value = this.consumeComponentValue(); do { token = this.consumeToken(); } while (token.type === 31 /* WHITESPACE_TOKEN */); if (token.type === 32 /* EOF_TOKEN */) { return value; } throw new SyntaxError("Error parsing CSS component value, multiple values found when expecting only one"); }; Parser.prototype.parseComponentValues = function () { var values = []; while (true) { var value = this.consumeComponentValue(); if (value.type === 32 /* EOF_TOKEN */) { return values; } values.push(value); values.push(); } }; Parser.prototype.consumeComponentValue = function () { var token = this.consumeToken(); switch (token.type) { case 11 /* LEFT_CURLY_BRACKET_TOKEN */: case 28 /* LEFT_SQUARE_BRACKET_TOKEN */: case 2 /* LEFT_PARENTHESIS_TOKEN */: return this.consumeSimpleBlock(token.type); case 19 /* FUNCTION_TOKEN */: return this.consumeFunction(token); } return token; }; Parser.prototype.consumeSimpleBlock = function (type) { var block = { type: type, values: [] }; var token = this.consumeToken(); while (true) { if (token.type === 32 /* EOF_TOKEN */ || isEndingTokenFor(token, type)) { return block; } this.reconsumeToken(token); block.values.push(this.consumeComponentValue()); token = this.consumeToken(); } }; Parser.prototype.consumeFunction = function (functionToken) { var cssFunction = { name: functionToken.value, values: [], type: 18 /* FUNCTION */ }; while (true) { var token = this.consumeToken(); if (token.type === 32 /* EOF_TOKEN */ || token.type === 3 /* RIGHT_PARENTHESIS_TOKEN */) { return cssFunction; } this.reconsumeToken(token); cssFunction.values.push(this.consumeComponentValue()); } }; Parser.prototype.consumeToken = function () { var token = this._tokens.shift(); return typeof token === 'undefined' ? EOF_TOKEN : token; }; Parser.prototype.reconsumeToken = function (token) { this._tokens.unshift(token); }; return Parser; }()); var isDimensionToken = function (token) { return token.type === 15 /* DIMENSION_TOKEN */; }; var isNumberToken = function (token) { return token.type === 17 /* NUMBER_TOKEN */; }; var isIdentToken = function (token) { return token.type === 20 /* IDENT_TOKEN */; }; var isStringToken = function (token) { return token.type === 0 /* STRING_TOKEN */; }; var isIdentWithValue = function (token, value) { return isIdentToken(token) && token.value === value; }; var nonWhiteSpace = function (token) { return token.type !== 31 /* WHITESPACE_TOKEN */; }; var nonFunctionArgSeparator = function (token) { return token.type !== 31 /* WHITESPACE_TOKEN */ && token.type !== 4 /* COMMA_TOKEN */; }; var parseFunctionArgs = function (tokens) { var args = []; var arg = []; tokens.forEach(function (token) { if (token.type === 4 /* COMMA_TOKEN */) { if (arg.length === 0) { throw new Error("Error parsing function args, zero tokens for arg"); } args.push(arg); arg = []; return; } if (token.type !== 31 /* WHITESPACE_TOKEN */) { arg.push(token); } }); if (arg.length) { args.push(arg); } return args; }; var isEndingTokenFor = function (token, type) { if (type === 11 /* LEFT_CURLY_BRACKET_TOKEN */ && token.type === 12 /* RIGHT_CURLY_BRACKET_TOKEN */) { return true; } if (type === 28 /* LEFT_SQUARE_BRACKET_TOKEN */ && token.type === 29 /* RIGHT_SQUARE_BRACKET_TOKEN */) { return true; } return type === 2 /* LEFT_PARENTHESIS_TOKEN */ && token.type === 3 /* RIGHT_PARENTHESIS_TOKEN */; }; var isLength = function (token) { return token.type === 17 /* NUMBER_TOKEN */ || token.type === 15 /* DIMENSION_TOKEN */; }; var isLengthPercentage = function (token) { return token.type === 16 /* PERCENTAGE_TOKEN */ || isLength(token); }; var parseLengthPercentageTuple = function (tokens) { return tokens.length > 1 ? [tokens[0], tokens[1]] : [tokens[0]]; }; var ZERO_LENGTH = { type: 17 /* NUMBER_TOKEN */, number: 0, flags: FLAG_INTEGER }; var FIFTY_PERCENT = { type: 16 /* PERCENTAGE_TOKEN */, number: 50, flags: FLAG_INTEGER }; var HUNDRED_PERCENT = { type: 16 /* PERCENTAGE_TOKEN */, number: 100, flags: FLAG_INTEGER }; var getAbsoluteValueForTuple = function (tuple, width, height) { var x = tuple[0], y = tuple[1]; return [getAbsoluteValue(x, width), getAbsoluteValue(typeof y !== 'undefined' ? y : x, height)]; }; var getAbsoluteValue = function (token, parent) { if (token.type === 16 /* PERCENTAGE_TOKEN */) { return (token.number / 100) * parent; } if (isDimensionToken(token)) { switch (token.unit) { case 'rem': case 'em': return 16 * token.number; // TODO use correct font-size case 'px': default: return token.number; } } return token.number; }; var DEG = 'deg'; var GRAD = 'grad'; var RAD = 'rad'; var TURN = 'turn'; var angle$1 = { name: 'angle', parse: function (_context, value) { if (value.type === 15 /* DIMENSION_TOKEN */) { switch (value.unit) { case DEG: return (Math.PI * value.number) / 180; case GRAD: return (Math.PI / 200) * value.number; case RAD: return value.number; case TURN: return Math.PI * 2 * value.number; } } throw new Error("Unsupported angle type"); } }; var isAngle = function (value) { if (value.type === 15 /* DIMENSION_TOKEN */) { if (value.unit === DEG || value.unit === GRAD || value.unit === RAD || value.unit === TURN) { return true; } } return false; }; var parseNamedSide = function (tokens) { var sideOrCorner = tokens .filter(isIdentToken) .map(function (ident) { return ident.value; }) .join(' '); switch (sideOrCorner) { case 'to bottom right': case 'to right bottom': case 'left top': case 'top left': return [ZERO_LENGTH, ZERO_LENGTH]; case 'to top': case 'bottom': return deg(0); case 'to bottom left': case 'to left bottom': case 'right top': case 'top right': return [ZERO_LENGTH, HUNDRED_PERCENT]; case 'to right': case 'left': return deg(90); case 'to top left': case 'to left top': case 'right bottom': case 'bottom right': return [HUNDRED_PERCENT, HUNDRED_PERCENT]; case 'to bottom': case 'top': return deg(180); case 'to top right': case 'to right top': case 'left bottom': case 'bottom left': return [HUNDRED_PERCENT, ZERO_LENGTH]; case 'to left': case 'right': return deg(270); } return 0; }; var deg = function (deg) { return (Math.PI * deg) / 180; }; var color$1 = { name: 'color', parse: function (context, value) { if (value.type === 18 /* FUNCTION */) { var colorFunction = SUPPORTED_COLOR_FUNCTIONS[value.name]; if (typeof colorFunction === 'undefined') { throw new Error("Attempting to parse an unsupported color function \"" + value.name + "\""); } return colorFunction(context, value.values); } if (value.type === 5 /* HASH_TOKEN */) { if (value.value.length === 3) { var r = value.value.substring(0, 1); var g = value.value.substring(1, 2); var b = value.value.substring(2, 3); return pack(parseInt(r + r, 16), parseInt(g + g, 16), parseInt(b + b, 16), 1); } if (value.value.length === 4) { var r = value.value.substring(0, 1); var g = value.value.substring(1, 2); var b = value.value.substring(2, 3); var a = value.value.substring(3, 4); return pack(parseInt(r + r, 16), parseInt(g + g, 16), parseInt(b + b, 16), parseInt(a + a, 16) / 255); } if (value.value.length === 6) { var r = value.value.substring(0, 2); var g = value.value.substring(2, 4); var b = value.value.substring(4, 6); return pack(parseInt(r, 16), parseInt(g, 16), parseInt(b, 16), 1); } if (value.value.length === 8) { var r = value.value.substring(0, 2); var g = value.value.substring(2, 4); var b = value.value.substring(4, 6); var a = value.value.substring(6, 8); return pack(parseInt(r, 16), parseInt(g, 16), parseInt(b, 16), parseInt(a, 16) / 255); } } if (value.type === 20 /* IDENT_TOKEN */) { var namedColor = COLORS[value.value.toUpperCase()]; if (typeof namedColor !== 'undefined') { return namedColor; } } return COLORS.TRANSPARENT; } }; var isTransparent = function (color) { return (0xff & color) === 0; }; var asString = function (color) { var alpha = 0xff & color; var blue = 0xff & (color >> 8); var green = 0xff & (color >> 16); var red = 0xff & (color >> 24); return alpha < 255 ? "rgba(" + red + "," + green + "," + blue + "," + alpha / 255 + ")" : "rgb(" + red + "," + green + "," + blue + ")"; }; var pack = function (r, g, b, a) { return ((r << 24) | (g << 16) | (b << 8) | (Math.round(a * 255) << 0)) >>> 0; }; var getTokenColorValue = function (token, i) { if (token.type === 17 /* NUMBER_TOKEN */) { return token.number; } if (token.type === 16 /* PERCENTAGE_TOKEN */) { var max = i === 3 ? 1 : 255; return i === 3 ? (token.number / 100) * max : Math.round((token.number / 100) * max); } return 0; }; var rgb = function (_context, args) { var tokens = args.filter(nonFunctionArgSeparator); if (tokens.length === 3) { var _a = tokens.map(getTokenColorValue), r = _a[0], g = _a[1], b = _a[2]; return pack(r, g, b, 1); } if (tokens.length === 4) { var _b = tokens.map(getTokenColorValue), r = _b[0], g = _b[1], b = _b[2], a = _b[3]; return pack(r, g, b, a); } return 0; }; function hue2rgb(t1, t2, hue) { if (hue < 0) { hue += 1; } if (hue >= 1) { hue -= 1; } if (hue < 1 / 6) { return (t2 - t1) * hue * 6 + t1; } else if (hue < 1 / 2) { return t2; } else if (hue < 2 / 3) { return (t2 - t1) * 6 * (2 / 3 - hue) + t1; } else { return t1; } } var hsl = function (context, args) { var tokens = args.filter(nonFunctionArgSeparator); var hue = tokens[0], saturation = tokens[1], lightness = tokens[2], alpha = tokens[3]; var h = (hue.type === 17 /* NUMBER_TOKEN */ ? deg(hue.number) : angle$1.parse(context, hue)) / (Math.PI * 2); var s = isLengthPercentage(saturation) ? saturation.number / 100 : 0; var l = isLengthPercentage(lightness) ? lightness.number / 100 : 0; var a = typeof alpha !== 'undefined' && isLengthPercentage(alpha) ? getAbsoluteValue(alpha, 1) : 1; if (s === 0) { return pack(l * 255, l * 255, l * 255, 1); } var t2 = l <= 0.5 ? l * (s + 1) : l + s - l * s; var t1 = l * 2 - t2; var r = hue2rgb(t1, t2, h + 1 / 3); var g = hue2rgb(t1, t2, h); var b = hue2rgb(t1, t2, h - 1 / 3); return pack(r * 255, g * 255, b * 255, a); }; var SUPPORTED_COLOR_FUNCTIONS = { hsl: hsl, hsla: hsl, rgb: rgb, rgba: rgb }; var parseColor = function (context, value) { return color$1.parse(context, Parser.create(value).parseComponentValue()); }; var COLORS = { ALICEBLUE: 0xf0f8ffff, ANTIQUEWHITE: 0xfaebd7ff, AQUA: 0x00ffffff, AQUAMARINE: 0x7fffd4ff, AZURE: 0xf0ffffff, BEIGE: 0xf5f5dcff, BISQUE: 0xffe4c4ff, BLACK: 0x000000ff, BLANCHEDALMOND: 0xffebcdff, BLUE: 0x0000ffff, BLUEVIOLET: 0x8a2be2ff, BROWN: 0xa52a2aff, BURLYWOOD: 0xdeb887ff, CADETBLUE: 0x5f9ea0ff, CHARTREUSE: 0x7fff00ff, CHOCOLATE: 0xd2691eff, CORAL: 0xff7f50ff, CORNFLOWERBLUE: 0x6495edff, CORNSILK: 0xfff8dcff, CRIMSON: 0xdc143cff, CYAN: 0x00ffffff, DARKBLUE: 0x00008bff, DARKCYAN: 0x008b8bff, DARKGOLDENROD: 0xb886bbff, DARKGRAY: 0xa9a9a9ff, DARKGREEN: 0x006400ff, DARKGREY: 0xa9a9a9ff, DARKKHAKI: 0xbdb76bff, DARKMAGENTA: 0x8b008bff, DARKOLIVEGREEN: 0x556b2fff, DARKORANGE: 0xff8c00ff, DARKORCHID: 0x9932ccff, DARKRED: 0x8b0000ff, DARKSALMON: 0xe9967aff, DARKSEAGREEN: 0x8fbc8fff, DARKSLATEBLUE: 0x483d8bff, DARKSLATEGRAY: 0x2f4f4fff, DARKSLATEGREY: 0x2f4f4fff, DARKTURQUOISE: 0x00ced1ff, DARKVIOLET: 0x9400d3ff, DEEPPINK: 0xff1493ff, DEEPSKYBLUE: 0x00bfffff, DIMGRAY: 0x696969ff, DIMGREY: 0x696969ff, DODGERBLUE: 0x1e90ffff, FIREBRICK: 0xb22222ff, FLORALWHITE: 0xfffaf0ff, FORESTGREEN: 0x228b22ff, FUCHSIA: 0xff00ffff, GAINSBORO: 0xdcdcdcff, GHOSTWHITE: 0xf8f8ffff, GOLD: 0xffd700ff, GOLDENROD: 0xdaa520ff, GRAY: 0x808080ff, GREEN: 0x008000ff, GREENYELLOW: 0xadff2fff, GREY: 0x808080ff, HONEYDEW: 0xf0fff0ff, HOTPINK: 0xff69b4ff, INDIANRED: 0xcd5c5cff, INDIGO: 0x4b0082ff, IVORY: 0xfffff0ff, KHAKI: 0xf0e68cff, LAVENDER: 0xe6e6faff, LAVENDERBLUSH: 0xfff0f5ff, LAWNGREEN: 0x7cfc00ff, LEMONCHIFFON: 0xfffacdff, LIGHTBLUE: 0xadd8e6ff, LIGHTCORAL: 0xf08080ff, LIGHTCYAN: 0xe0ffffff, LIGHTGOLDENRODYELLOW: 0xfafad2ff, LIGHTGRAY: 0xd3d3d3ff, LIGHTGREEN: 0x90ee90ff, LIGHTGREY: 0xd3d3d3ff, LIGHTPINK: 0xffb6c1ff, LIGHTSALMON: 0xffa07aff, LIGHTSEAGREEN: 0x20b2aaff, LIGHTSKYBLUE: 0x87cefaff, LIGHTSLATEGRAY: 0x778899ff, LIGHTSLATEGREY: 0x778899ff, LIGHTSTEELBLUE: 0xb0c4deff, LIGHTYELLOW: 0xffffe0ff, LIME: 0x00ff00ff, LIMEGREEN: 0x32cd32ff, LINEN: 0xfaf0e6ff, MAGENTA: 0xff00ffff, MAROON: 0x800000ff, MEDIUMAQUAMARINE: 0x66cdaaff, MEDIUMBLUE: 0x0000cdff, MEDIUMORCHID: 0xba55d3ff, MEDIUMPURPLE: 0x9370dbff, MEDIUMSEAGREEN: 0x3cb371ff, MEDIUMSLATEBLUE: 0x7b68eeff, MEDIUMSPRINGGREEN: 0x00fa9aff, MEDIUMTURQUOISE: 0x48d1ccff, MEDIUMVIOLETRED: 0xc71585ff, MIDNIGHTBLUE: 0x191970ff, MINTCREAM: 0xf5fffaff, MISTYROSE: 0xffe4e1ff, MOCCASIN: 0xffe4b5ff, NAVAJOWHITE: 0xffdeadff, NAVY: 0x000080ff, OLDLACE: 0xfdf5e6ff, OLIVE: 0x808000ff, OLIVEDRAB: 0x6b8e23ff, ORANGE: 0xffa500ff, ORANGERED: 0xff4500ff, ORCHID: 0xda70d6ff, PALEGOLDENROD: 0xeee8aaff, PALEGREEN: 0x98fb98ff, PALETURQUOISE: 0xafeeeeff, PALEVIOLETRED: 0xdb7093ff, PAPAYAWHIP: 0xffefd5ff, PEACHPUFF: 0xffdab9ff, PERU: 0xcd853fff, PINK: 0xffc0cbff, PLUM: 0xdda0ddff, POWDERBLUE: 0xb0e0e6ff, PURPLE: 0x800080ff, REBECCAPURPLE: 0x663399ff, RED: 0xff0000ff, ROSYBROWN: 0xbc8f8fff, ROYALBLUE: 0x4169e1ff, SADDLEBROWN: 0x8b4513ff, SALMON: 0xfa8072ff, SANDYBROWN: 0xf4a460ff, SEAGREEN: 0x2e8b57ff, SEASHELL: 0xfff5eeff, SIENNA: 0xa0522dff, SILVER: 0xc0c0c0ff, SKYBLUE: 0x87ceebff, SLATEBLUE: 0x6a5acdff, SLATEGRAY: 0x708090ff, SLATEGREY: 0x708090ff, SNOW: 0xfffafaff, SPRINGGREEN: 0x00ff7fff, STEELBLUE: 0x4682b4ff, TAN: 0xd2b48cff, TEAL: 0x008080ff, THISTLE: 0xd8bfd8ff, TOMATO: 0xff6347ff, TRANSPARENT: 0x00000000, TURQUOISE: 0x40e0d0ff, VIOLET: 0xee82eeff, WHEAT: 0xf5deb3ff, WHITE: 0xffffffff, WHITESMOKE: 0xf5f5f5ff, YELLOW: 0xffff00ff, YELLOWGREEN: 0x9acd32ff }; var backgroundClip = { name: 'background-clip', initialValue: 'border-box', prefix: false, type: 1 /* LIST */, parse: function (_context, tokens) { return tokens.map(function (token) { if (isIdentToken(token)) { switch (token.value) { case 'padding-box': return 1 /* PADDING_BOX */; case 'content-box': return 2 /* CONTENT_BOX */; } } return 0 /* BORDER_BOX */; }); } }; var backgroundColor = { name: "background-color", initialValue: 'transparent', prefix: false, type: 3 /* TYPE_VALUE */, format: 'color' }; var parseColorStop = function (context, args) { var color = color$1.parse(context, args[0]); var stop = args[1]; return stop && isLengthPercentage(stop) ? { color: color, stop: stop } : { color: color, stop: null }; }; var processColorStops = function (stops, lineLength) { var first = stops[0]; var last = stops[stops.length - 1]; if (first.stop === null) { first.stop = ZERO_LENGTH; } if (last.stop === null) { last.stop = HUNDRED_PERCENT; } var processStops = []; var previous = 0; for (var i = 0; i < stops.length; i++) { var stop_1 = stops[i].stop; if (stop_1 !== null) { var absoluteValue = getAbsoluteValue(stop_1, lineLength); if (absoluteValue > previous) { processStops.push(absoluteValue); } else { processStops.push(previous); } previous = absoluteValue; } else { processStops.push(null); } } var gapBegin = null; for (var i = 0; i < processStops.length; i++) { var stop_2 = processStops[i]; if (stop_2 === null) { if (gapBegin === null) { gapBegin = i; } } else if (gapBegin !== null) { var gapLength = i - gapBegin; var beforeGap = processStops[gapBegin - 1]; var gapValue = (stop_2 - beforeGap) / (gapLength + 1); for (var g = 1; g <= gapLength; g++) { processStops[gapBegin + g - 1] = gapValue * g; } gapBegin = null; } } return stops.map(function (_a, i) { var color = _a.color; return { color: color, stop: Math.max(Math.min(1, processStops[i] / lineLength), 0) }; }); }; var getAngleFromCorner = function (corner, width, height) { var centerX = width / 2; var centerY = height / 2; var x = getAbsoluteValue(corner[0], width) - centerX; var y = centerY - getAbsoluteValue(corner[1], height); return (Math.atan2(y, x) + Math.PI * 2) % (Math.PI * 2); }; var calculateGradientDirection = function (angle, width, height) { var radian = typeof angle === 'number' ? angle : getAngleFromCorner(angle, width, height); var lineLength = Math.abs(width * Math.sin(radian)) + Math.abs(height * Math.cos(radian)); var halfWidth = width / 2; var halfHeight = height / 2; var halfLineLength = lineLength / 2; var yDiff = Math.sin(radian - Math.PI / 2) * halfLineLength; var xDiff = Math.cos(radian - Math.PI / 2) * halfLineLength; return [lineLength, halfWidth - xDiff, halfWidth + xDiff, halfHeight - yDiff, halfHeight + yDiff]; }; var distance = function (a, b) { return Math.sqrt(a * a + b * b); }; var findCorner = function (width, height, x, y, closest) { var corners = [ [0, 0], [0, height], [width, 0], [width, height] ]; return corners.reduce(function (stat, corner) { var cx = corner[0], cy = corner[1]; var d = distance(x - cx, y - cy); if (closest ? d < stat.optimumDistance : d > stat.optimumDistance) { return { optimumCorner: corner, optimumDistance: d }; } return stat; }, { optimumDistance: closest ? Infinity : -Infinity, optimumCorner: null }).optimumCorner; }; var calculateRadius = function (gradient, x, y, width, height) { var rx = 0; var ry = 0; switch (gradient.size) { case 0 /* CLOSEST_SIDE */: // The ending shape is sized so that that it exactly meets the side of the gradient box closest to the gradient’s center. // If the shape is an ellipse, it exactly meets the closest side in each dimension. if (gradient.shape === 0 /* CIRCLE */) { rx = ry = Math.min(Math.abs(x), Math.abs(x - width), Math.abs(y), Math.abs(y - height)); } else if (gradient.shape === 1 /* ELLIPSE */) { rx = Math.min(Math.abs(x), Math.abs(x - width)); ry = Math.min(Math.abs(y), Math.abs(y - height)); } break; case 2 /* CLOSEST_CORNER */: // The ending shape is sized so that that it passes through the corner of the gradient box closest to the gradient’s center. // If the shape is an ellipse, the ending shape is given the same aspect-ratio it would have if closest-side were specified. if (gradient.shape === 0 /* CIRCLE */) { rx = ry = Math.min(distance(x, y), distance(x, y - height), distance(x - width, y), distance(x - width, y - height)); } else if (gradient.shape === 1 /* ELLIPSE */) { // Compute the ratio ry/rx (which is to be the same as for "closest-side") var c = Math.min(Math.abs(y), Math.abs(y - height)) / Math.min(Math.abs(x), Math.abs(x - width)); var _a = findCorner(width, height, x, y, true), cx = _a[0], cy = _a[1]; rx = distance(cx - x, (cy - y) / c); ry = c * rx; } break; case 1 /* FARTHEST_SIDE */: // Same as closest-side, except the ending shape is sized based on the farthest side(s) if (gradient.shape === 0 /* CIRCLE */) { rx = ry = Math.max(Math.abs(x), Math.abs(x - width), Math.abs(y), Math.abs(y - height)); } else if (gradient.shape === 1 /* ELLIPSE */) { rx = Math.max(Math.abs(x), Math.abs(x - width)); ry = Math.max(Math.abs(y), Math.abs(y - height)); } break; case 3 /* FARTHEST_CORNER */: // Same as closest-corner, except the ending shape is sized based on the farthest corner. // If the shape is an ellipse, the ending shape is given the same aspect ratio it would have if farthest-side were specified. if (gradient.shape === 0 /* CIRCLE */) { rx = ry = Math.max(distance(x, y), distance(x, y - height), distance(x - width, y), distance(x - width, y - height)); } else if (gradient.shape === 1 /* ELLIPSE */) { // Compute the ratio ry/rx (which is to be the same as for "farthest-side") var c = Math.max(Math.abs(y), Math.abs(y - height)) / Math.max(Math.abs(x), Math.abs(x - width)); var _b = findCorner(width, height, x, y, false), cx = _b[0], cy = _b[1]; rx = distance(cx - x, (cy - y) / c); ry = c * rx; } break; } if (Array.isArray(gradient.size)) { rx = getAbsoluteValue(gradient.size[0], width); ry = gradient.size.length === 2 ? getAbsoluteValue(gradient.size[1], height) : rx; } return [rx, ry]; }; var linearGradient = function (context, tokens) { var angle$1$1 = deg(180); var stops = []; parseFunctionArgs(tokens).forEach(function (arg, i) { if (i === 0) { var firstToken = arg[0]; if (firstToken.type === 20 /* IDENT_TOKEN */ && firstToken.value === 'to') { angle$1$1 = parseNamedSide(arg); return; } else if (isAngle(firstToken)) { angle$1$1 = angle$1.parse(context, firstToken); return; } } var colorStop = parseColorStop(context, arg); stops.push(colorStop); }); return { angle: angle$1$1, stops: stops, type: 1 /* LINEAR_GRADIENT */ }; }; var prefixLinearGradient = function (context, tokens) { var angle$1$1 = deg(180); var stops = []; parseFunctionArgs(tokens).forEach(function (arg, i) { if (i === 0) { var firstToken = arg[0]; if (firstToken.type === 20 /* IDENT_TOKEN */ && ['top', 'left', 'right', 'bottom'].indexOf(firstToken.value) !== -1) { angle$1$1 = parseNamedSide(arg); return; } else if (isAngle(firstToken)) { angle$1$1 = (angle$1.parse(context, firstToken) + deg(270)) % deg(360); return; } } var colorStop = parseColorStop(context, arg); stops.push(colorStop); }); return { angle: angle$1$1, stops: stops, type: 1 /* LINEAR_GRADIENT */ }; }; var webkitGradient = function (context, tokens) { var angle = deg(180); var stops = []; var type = 1 /* LINEAR_GRADIENT */; var shape = 0 /* CIRCLE */; var size = 3 /* FARTHEST_CORNER */; var position = []; parseFunctionArgs(tokens).forEach(function (arg, i) { var firstToken = arg[0]; if (i === 0) { if (isIdentToken(firstToken) && firstToken.value === 'linear') { type = 1 /* LINEAR_GRADIENT */; return; } else if (isIdentToken(firstToken) && firstToken.value === 'radial') { type = 2 /* RADIAL_GRADIENT */; return; } } if (firstToken.type === 18 /* FUNCTION */) { if (firstToken.name === 'from') { var color = color$1.parse(context, firstToken.values[0]); stops.push({ stop: ZERO_LENGTH, color: color }); } else if (firstToken.name === 'to') { var color = color$1.parse(context, firstToken.values[0]); stops.push({ stop: HUNDRED_PERCENT, color: color }); } else if (firstToken.name === 'color-stop') { var values = firstToken.values.filter(nonFunctionArgSeparator); if (values.length === 2) { var color = color$1.parse(context, values[1]); var stop_1 = values[0]; if (isNumberToken(stop_1)) { stops.push({ stop: { type: 16 /* PERCENTAGE_TOKEN */, number: stop_1.number * 100, flags: stop_1.flags }, color: color }); } } } } }); return type === 1 /* LINEAR_GRADIENT */ ? { angle: (angle + deg(180)) % deg(360), stops: stops, type: type } : { size: size, shape: shape, stops: stops, position: position, type: type }; }; var CLOSEST_SIDE = 'closest-side'; var FARTHEST_SIDE = 'farthest-side'; var CLOSEST_CORNER = 'closest-corner'; var FARTHEST_CORNER = 'farthest-corner'; var CIRCLE = 'circle'; var ELLIPSE = 'ellipse'; var COVER = 'cover'; var CONTAIN = 'contain'; var radialGradient = function (context, tokens) { var shape = 0 /* CIRCLE */; var size = 3 /* FARTHEST_CORNER */; var stops = []; var position = []; parseFunctionArgs(tokens).forEach(function (arg, i) { var isColorStop = true; if (i === 0) { var isAtPosition_1 = false; isColorStop = arg.reduce(function (acc, token) { if (isAtPosition_1) { if (isIdentToken(token)) { switch (token.value) { case 'center': position.push(FIFTY_PERCENT); return acc; case 'top': case 'left': position.push(ZERO_LENGTH); return acc; case 'right': case 'bottom': position.push(HUNDRED_PERCENT); return acc; } } else if (isLengthPercentage(token) || isLength(token)) { position.push(token); } } else if (isIdentToken(token)) { switch (token.value) { case CIRCLE: shape = 0 /* CIRCLE */; return false; case ELLIPSE: shape = 1 /* ELLIPSE */; return false; case 'at': isAtPosition_1 = true; return false; case CLOSEST_SIDE: size = 0 /* CLOSEST_SIDE */; return false; case COVER: case FARTHEST_SIDE: size = 1 /* FARTHEST_SIDE */; return false; case CONTAIN: case CLOSEST_CORNER: size = 2 /* CLOSEST_CORNER */; return false; case FARTHEST_CORNER: size = 3 /* FARTHEST_CORNER */; return false; } } else if (isLength(token) || isLengthPercentage(token)) { if (!Array.isArray(size)) { size = []; } size.push(token); return false; } return acc; }, isColorStop); } if (isColorStop) { var colorStop = parseColorStop(context, arg); stops.push(colorStop); } }); return { size: size, shape: shape, stops: stops, position: position, type: 2 /* RADIAL_GRADIENT */ }; }; var prefixRadialGradient = function (context, tokens) { var shape = 0 /* CIRCLE */; var size = 3 /* FARTHEST_CORNER */; var stops = []; var position = []; parseFunctionArgs(tokens).forEach(function (arg, i) { var isColorStop = true; if (i === 0) { isColorStop = arg.reduce(function (acc, token) { if (isIdentToken(token)) { switch (token.value) { case 'center': position.push(FIFTY_PERCENT); return false; case 'top': case 'left': position.push(ZERO_LENGTH); return false; case 'right': case 'bottom': position.push(HUNDRED_PERCENT); return false; } } else if (isLengthPercentage(token) || isLength(token)) { position.push(token); return false; } return acc; }, isColorStop); } else if (i === 1) { isColorStop = arg.reduce(function (acc, token) { if (isIdentToken(token)) { switch (token.value) { case CIRCLE: shape = 0 /* CIRCLE */; return false; case ELLIPSE: shape = 1 /* ELLIPSE */; return false; case CONTAIN: case CLOSEST_SIDE: size = 0 /* CLOSEST_SIDE */; return false; case FARTHEST_SIDE: size = 1 /* FARTHEST_SIDE */; return false; case CLOSEST_CORNER: size = 2 /* CLOSEST_CORNER */; return false; case COVER: case FARTHEST_CORNER: size = 3 /* FARTHEST_CORNER */; return false; } } else if (isLength(token) || isLengthPercentage(token)) { if (!Array.isArray(size)) { size = []; } size.push(token); return false; } return acc; }, isColorStop); } if (isColorStop) { var colorStop = parseColorStop(context, arg); stops.push(colorStop); } }); return { size: size, shape: shape, stops: stops, position: position, type: 2 /* RADIAL_GRADIENT */ }; }; var isLinearGradient = function (background) { return background.type === 1 /* LINEAR_GRADIENT */; }; var isRadialGradient = function (background) { return background.type === 2 /* RADIAL_GRADIENT */; }; var image$1 = { name: 'image', parse: function (context, value) { if (value.type === 22 /* URL_TOKEN */) { var image_1 = { url: value.value, type: 0 /* URL */ }; context.cache.addImage(value.value); return image_1; } if (value.type === 18 /* FUNCTION */) { var imageFunction = SUPPORTED_IMAGE_FUNCTIONS[value.name]; if (typeof imageFunction === 'undefined') { throw new Error("Attempting to parse an unsupported image function \"" + value.name + "\""); } return imageFunction(context, value.values); } throw new Error("Unsupported image type " + value.type); } }; function isSupportedImage(value) { return (!(value.type === 20 /* IDENT_TOKEN */ && value.value === 'none') && (value.type !== 18 /* FUNCTION */ || !!SUPPORTED_IMAGE_FUNCTIONS[value.name])); } var SUPPORTED_IMAGE_FUNCTIONS = { 'linear-gradient': linearGradient, '-moz-linear-gradient': prefixLinearGradient, '-ms-linear-gradient': prefixLinearGradient, '-o-linear-gradient': prefixLinearGradient, '-webkit-linear-gradient': prefixLinearGradient, 'radial-gradient': radialGradient, '-moz-radial-gradient': prefixRadialGradient, '-ms-radial-gradient': prefixRadialGradient, '-o-radial-gradient': prefixRadialGradient, '-webkit-radial-gradient': prefixRadialGradient, '-webkit-gradient': webkitGradient }; var backgroundImage = { name: 'background-image', initialValue: 'none', type: 1 /* LIST */, prefix: false, parse: function (context, tokens) { if (tokens.length === 0) { return []; } var first = tokens[0]; if (first.type === 20 /* IDENT_TOKEN */ && first.value === 'none') { return []; } return tokens .filter(function (value) { return nonFunctionArgSeparator(value) && isSupportedImage(value); }) .map(function (value) { return image$1.parse(context, value); }); } }; var backgroundOrigin = { name: 'background-origin', initialValue: 'border-box', prefix: false, type: 1 /* LIST */, parse: function (_context, tokens) { return tokens.map(function (token) { if (isIdentToken(token)) { switch (token.value) { case 'padding-box': return 1 /* PADDING_BOX */; case 'content-box': return 2 /* CONTENT_BOX */; } } return 0 /* BORDER_BOX */; }); } }; var backgroundPosition = { name: 'background-position', initialValue: '0% 0%', type: 1 /* LIST */, prefix: false, parse: function (_context, tokens) { return parseFunctionArgs(tokens) .map(function (values) { return values.filter(isLengthPercentage); }) .map(parseLengthPercentageTuple); } }; var backgroundRepeat = { name: 'background-repeat', initialValue: 'repeat', prefix: false, type: 1 /* LIST */, parse: function (_context, tokens) { return parseFunctionArgs(tokens) .map(function (values) { return values .filter(isIdentToken) .map(function (token) { return token.value; }) .join(' '); }) .map(parseBackgroundRepeat); } }; var parseBackgroundRepeat = function (value) { switch (value) { case 'no-repeat': return 1 /* NO_REPEAT */; case 'repeat-x': case 'repeat no-repeat': return 2 /* REPEAT_X */; case 'repeat-y': case 'no-repeat repeat': return 3 /* REPEAT_Y */; case 'repeat': default: return 0 /* REPEAT */; } }; var BACKGROUND_SIZE; (function (BACKGROUND_SIZE) { BACKGROUND_SIZE["AUTO"] = "auto"; BACKGROUND_SIZE["CONTAIN"] = "contain"; BACKGROUND_SIZE["COVER"] = "cover"; })(BACKGROUND_SIZE || (BACKGROUND_SIZE = {})); var backgroundSize = { name: 'background-size', initialValue: '0', prefix: false, type: 1 /* LIST */, parse: function (_context, tokens) { return parseFunctionArgs(tokens).map(function (values) { return values.filter(isBackgroundSizeInfoToken); }); } }; var isBackgroundSizeInfoToken = function (value) { return isIdentToken(value) || isLengthPercentage(value); }; var borderColorForSide = function (side) { return ({ name: "border-" + side + "-color", initialValue: 'transparent', prefix: false, type: 3 /* TYPE_VALUE */, format: 'color' }); }; var borderTopColor = borderColorForSide('top'); var borderRightColor = borderColorForSide('right'); var borderBottomColor = borderColorForSide('bottom'); var borderLeftColor = borderColorForSide('left'); var borderRadiusForSide = function (side) { return ({ name: "border-radius-" + side, initialValue: '0 0', prefix: false, type: 1 /* LIST */, parse: function (_context, tokens) { return parseLengthPercentageTuple(tokens.filter(isLengthPercentage)); } }); }; var borderTopLeftRadius = borderRadiusForSide('top-left'); var borderTopRightRadius = borderRadiusForSide('top-right'); var borderBottomRightRadius = borderRadiusForSide('bottom-right'); var borderBottomLeftRadius = borderRadiusForSide('bottom-left'); var borderStyleForSide = function (side) { return ({ name: "border-" + side + "-style", initialValue: 'solid', prefix: false, type: 2 /* IDENT_VALUE */, parse: function (_context, style) { switch (style) { case 'none': return 0 /* NONE */; case 'dashed': return 2 /* DASHED */; case 'dotted': return 3 /* DOTTED */; case 'double': return 4 /* DOUBLE */; } return 1 /* SOLID */; } }); }; var borderTopStyle = borderStyleForSide('top'); var borderRightStyle = borderStyleForSide('right'); var borderBottomStyle = borderStyleForSide('bottom'); var borderLeftStyle = borderStyleForSide('left'); var borderWidthForSide = function (side) { return ({ name: "border-" + side + "-width", initialValue: '0', type: 0 /* VALUE */, prefix: false, parse: function (_context, token) { if (isDimensionToken(token)) { return token.number; } return 0; } }); }; var borderTopWidth = borderWidthForSide('top'); var borderRightWidth = borderWidthForSide('right'); var borderBottomWidth = borderWidthForSide('bottom'); var borderLeftWidth = borderWidthForSide('left'); var color = { name: "color", initialValue: 'transparent', prefix: false, type: 3 /* TYPE_VALUE */, format: 'color' }; var direction = { name: 'direction', initialValue: 'ltr', prefix: false, type: 2 /* IDENT_VALUE */, parse: function (_context, direction) { switch (direction) { case 'rtl': return 1 /* RTL */; case 'ltr': default: return 0 /* LTR */; } } }; var display = { name: 'display', initialValue: 'inline-block', prefix: false, type: 1 /* LIST */, parse: function (_context, tokens) { return tokens.filter(isIdentToken).reduce(function (bit, token) { return bit | parseDisplayValue(token.value); }, 0 /* NONE */); } }; var parseDisplayValue = function (display) { switch (display) { case 'block': case '-webkit-box': return 2 /* BLOCK */; case 'inline': return 4 /* INLINE */; case 'run-in': return 8 /* RUN_IN */; case 'flow': return 16 /* FLOW */; case 'flow-root': return 32 /* FLOW_ROOT */; case 'table': return 64 /* TABLE */; case 'flex': case '-webkit-flex': return 128 /* FLEX */; case 'grid': case '-ms-grid': return 256 /* GRID */; case 'ruby': return 512 /* RUBY */; case 'subgrid': return 1024 /* SUBGRID */; case 'list-item': return 2048 /* LIST_ITEM */; case 'table-row-group': return 4096 /* TABLE_ROW_GROUP */; case 'table-header-group': return 8192 /* TABLE_HEADER_GROUP */; case 'table-footer-group': return 16384 /* TABLE_FOOTER_GROUP */; case 'table-row': return 32768 /* TABLE_ROW */; case 'table-cell': return 65536 /* TABLE_CELL */; case 'table-column-group': return 131072 /* TABLE_COLUMN_GROUP */; case 'table-column': return 262144 /* TABLE_COLUMN */; case 'table-caption': return 524288 /* TABLE_CAPTION */; case 'ruby-base': return 1048576 /* RUBY_BASE */; case 'ruby-text': return 2097152 /* RUBY_TEXT */; case 'ruby-base-container': return 4194304 /* RUBY_BASE_CONTAINER */; case 'ruby-text-container': return 8388608 /* RUBY_TEXT_CONTAINER */; case 'contents': return 16777216 /* CONTENTS */; case 'inline-block': return 33554432 /* INLINE_BLOCK */; case 'inline-list-item': return 67108864 /* INLINE_LIST_ITEM */; case 'inline-table': return 134217728 /* INLINE_TABLE */; case 'inline-flex': return 268435456 /* INLINE_FLEX */; case 'inline-grid': return 536870912 /* INLINE_GRID */; } return 0 /* NONE */; }; var float = { name: 'float', initialValue: 'none', prefix: false, type: 2 /* IDENT_VALUE */, parse: function (_context, float) { switch (float) { case 'left': return 1 /* LEFT */; case 'right': return 2 /* RIGHT */; case 'inline-start': return 3 /* INLINE_START */; case 'inline-end': return 4 /* INLINE_END */; } return 0 /* NONE */; } }; var letterSpacing = { name: 'letter-spacing', initialValue: '0', prefix: false, type: 0 /* VALUE */, parse: function (_context, token) { if (token.type === 20 /* IDENT_TOKEN */ && token.value === 'normal') { return 0; } if (token.type === 17 /* NUMBER_TOKEN */) { return token.number; } if (token.type === 15 /* DIMENSION_TOKEN */) { return token.number; } return 0; } }; var LINE_BREAK; (function (LINE_BREAK) { LINE_BREAK["NORMAL"] = "normal"; LINE_BREAK["STRICT"] = "strict"; })(LINE_BREAK || (LINE_BREAK = {})); var lineBreak = { name: 'line-break', initialValue: 'normal', prefix: false, type: 2 /* IDENT_VALUE */, parse: function (_context, lineBreak) { switch (lineBreak) { case 'strict': return LINE_BREAK.STRICT; case 'normal': default: return LINE_BREAK.NORMAL; } } }; var lineHeight = { name: 'line-height', initialValue: 'normal', prefix: false, type: 4 /* TOKEN_VALUE */ }; var computeLineHeight = function (token, fontSize) { if (isIdentToken(token) && token.value === 'normal') { return 1.2 * fontSize; } else if (token.type === 17 /* NUMBER_TOKEN */) { return fontSize * token.number; } else if (isLengthPercentage(token)) { return getAbsoluteValue(token, fontSize); } return fontSize; }; var listStyleImage = { name: 'list-style-image', initialValue: 'none', type: 0 /* VALUE */, prefix: false, parse: function (context, token) { if (token.type === 20 /* IDENT_TOKEN */ && token.value === 'none') { return null; } return image$1.parse(context, token); } }; var listStylePosition = { name: 'list-style-position', initialValue: 'outside', prefix: false, type: 2 /* IDENT_VALUE */, parse: function (_context, position) { switch (position) { case 'inside': return 0 /* INSIDE */; case 'outside': default: return 1 /* OUTSIDE */; } } }; var listStyleType = { name: 'list-style-type', initialValue: 'none', prefix: false, type: 2 /* IDENT_VALUE */, parse: function (_context, type) { switch (type) { case 'disc': return 0 /* DISC */; case 'circle': return 1 /* CIRCLE */; case 'square': return 2 /* SQUARE */; case 'decimal': return 3 /* DECIMAL */; case 'cjk-decimal': return 4 /* CJK_DECIMAL */; case 'decimal-leading-zero': return 5 /* DECIMAL_LEADING_ZERO */; case 'lower-roman': return 6 /* LOWER_ROMAN */; case 'upper-roman': return 7 /* UPPER_ROMAN */; case 'lower-greek': return 8 /* LOWER_GREEK */; case 'lower-alpha': return 9 /* LOWER_ALPHA */; case 'upper-alpha': return 10 /* UPPER_ALPHA */; case 'arabic-indic': return 11 /* ARABIC_INDIC */; case 'armenian': return 12 /* ARMENIAN */; case 'bengali': return 13 /* BENGALI */; case 'cambodian': return 14 /* CAMBODIAN */; case 'cjk-earthly-branch': return 15 /* CJK_EARTHLY_BRANCH */; case 'cjk-heavenly-stem': return 16 /* CJK_HEAVENLY_STEM */; case 'cjk-ideographic': return 17 /* CJK_IDEOGRAPHIC */; case 'devanagari': return 18 /* DEVANAGARI */; case 'ethiopic-numeric': return 19 /* ETHIOPIC_NUMERIC */; case 'georgian': return 20 /* GEORGIAN */; case 'gujarati': return 21 /* GUJARATI */; case 'gurmukhi': return 22 /* GURMUKHI */; case 'hebrew': return 22 /* HEBREW */; case 'hiragana': return 23 /* HIRAGANA */; case 'hiragana-iroha': return 24 /* HIRAGANA_IROHA */; case 'japanese-formal': return 25 /* JAPANESE_FORMAL */; case 'japanese-informal': return 26 /* JAPANESE_INFORMAL */; case 'kannada': return 27 /* KANNADA */; case 'katakana': return 28 /* KATAKANA */; case 'katakana-iroha': return 29 /* KATAKANA_IROHA */; case 'khmer': return 30 /* KHMER */; case 'korean-hangul-formal': return 31 /* KOREAN_HANGUL_FORMAL */; case 'korean-hanja-formal': return 32 /* KOREAN_HANJA_FORMAL */; case 'korean-hanja-informal': return 33 /* KOREAN_HANJA_INFORMAL */; case 'lao': return 34 /* LAO */; case 'lower-armenian': return 35 /* LOWER_ARMENIAN */; case 'malayalam': return 36 /* MALAYALAM */; case 'mongolian': return 37 /* MONGOLIAN */; case 'myanmar': return 38 /* MYANMAR */; case 'oriya': return 39 /* ORIYA */; case 'persian': return 40 /* PERSIAN */; case 'simp-chinese-formal': return 41 /* SIMP_CHINESE_FORMAL */; case 'simp-chinese-informal': return 42 /* SIMP_CHINESE_INFORMAL */; case 'tamil': return 43 /* TAMIL */; case 'telugu': return 44 /* TELUGU */; case 'thai': return 45 /* THAI */; case 'tibetan': return 46 /* TIBETAN */; case 'trad-chinese-formal': return 47 /* TRAD_CHINESE_FORMAL */; case 'trad-chinese-informal': return 48 /* TRAD_CHINESE_INFORMAL */; case 'upper-armenian': return 49 /* UPPER_ARMENIAN */; case 'disclosure-open': return 50 /* DISCLOSURE_OPEN */; case 'disclosure-closed': return 51 /* DISCLOSURE_CLOSED */; case 'none': default: return -1 /* NONE */; } } }; var marginForSide = function (side) { return ({ name: "margin-" + side, initialValue: '0', prefix: false, type: 4 /* TOKEN_VALUE */ }); }; var marginTop = marginForSide('top'); var marginRight = marginForSide('right'); var marginBottom = marginForSide('bottom'); var marginLeft = marginForSide('left'); var overflow = { name: 'overflow', initialValue: 'visible', prefix: false, type: 1 /* LIST */, parse: function (_context, tokens) { return tokens.filter(isIdentToken).map(function (overflow) { switch (overflow.value) { case 'hidden': return 1 /* HIDDEN */; case 'scroll': return 2 /* SCROLL */; case 'clip': return 3 /* CLIP */; case 'auto': return 4 /* AUTO */; case 'visible': default: return 0 /* VISIBLE */; } }); } }; var overflowWrap = { name: 'overflow-wrap', initialValue: 'normal', prefix: false, type: 2 /* IDENT_VALUE */, parse: function (_context, overflow) { switch (overflow) { case 'break-word': return "break-word" /* BREAK_WORD */; case 'normal': default: return "normal" /* NORMAL */; } } }; var paddingForSide = function (side) { return ({ name: "padding-" + side, initialValue: '0', prefix: false, type: 3 /* TYPE_VALUE */, format: 'length-percentage' }); }; var paddingTop = paddingForSide('top'); var paddingRight = paddingForSide('right'); var paddingBottom = paddingForSide('bottom'); var paddingLeft = paddingForSide('left'); var textAlign = { name: 'text-align', initialValue: 'left', prefix: false, type: 2 /* IDENT_VALUE */, parse: function (_context, textAlign) { switch (textAlign) { case 'right': return 2 /* RIGHT */; case 'center': case 'justify': return 1 /* CENTER */; case 'left': default: return 0 /* LEFT */; } } }; var position = { name: 'position', initialValue: 'static', prefix: false, type: 2 /* IDENT_VALUE */, parse: function (_context, position) { switch (position) { case 'relative': return 1 /* RELATIVE */; case 'absolute': return 2 /* ABSOLUTE */; case 'fixed': return 3 /* FIXED */; case 'sticky': return 4 /* STICKY */; } return 0 /* STATIC */; } }; var textShadow = { name: 'text-shadow', initialValue: 'none', type: 1 /* LIST */, prefix: false, parse: function (context, tokens) { if (tokens.length === 1 && isIdentWithValue(tokens[0], 'none')) { return []; } return parseFunctionArgs(tokens).map(function (values) { var shadow = { color: COLORS.TRANSPARENT, offsetX: ZERO_LENGTH, offsetY: ZERO_LENGTH, blur: ZERO_LENGTH }; var c = 0; for (var i = 0; i < values.length; i++) { var token = values[i]; if (isLength(token)) { if (c === 0) { shadow.offsetX = token; } else if (c === 1) { shadow.offsetY = token; } else { shadow.blur = token; } c++; } else { shadow.color = color$1.parse(context, token); } } return shadow; }); } }; var textTransform = { name: 'text-transform', initialValue: 'none', prefix: false, type: 2 /* IDENT_VALUE */, parse: function (_context, textTransform) { switch (textTransform) { case 'uppercase': return 2 /* UPPERCASE */; case 'lowercase': return 1 /* LOWERCASE */; case 'capitalize': return 3 /* CAPITALIZE */; } return 0 /* NONE */; } }; var transform$1 = { name: 'transform', initialValue: 'none', prefix: true, type: 0 /* VALUE */, parse: function (_context, token) { if (token.type === 20 /* IDENT_TOKEN */ && token.value === 'none') { return null; } if (token.type === 18 /* FUNCTION */) { var transformFunction = SUPPORTED_TRANSFORM_FUNCTIONS[token.name]; if (typeof transformFunction === 'undefined') { throw new Error("Attempting to parse an unsupported transform function \"" + token.name + "\""); } return transformFunction(token.values); } return null; } }; var matrix = function (args) { var values = args.filter(function (arg) { return arg.type === 17 /* NUMBER_TOKEN */; }).map(function (arg) { return arg.number; }); return values.length === 6 ? values : null; }; // doesn't support 3D transforms at the moment var matrix3d = function (args) { var values = args.filter(function (arg) { return arg.type === 17 /* NUMBER_TOKEN */; }).map(function (arg) { return arg.number; }); var a1 = values[0], b1 = values[1]; values[2]; values[3]; var a2 = values[4], b2 = values[5]; values[6]; values[7]; values[8]; values[9]; values[10]; values[11]; var a4 = values[12], b4 = values[13]; values[14]; values[15]; return values.length === 16 ? [a1, b1, a2, b2, a4, b4] : null; }; var SUPPORTED_TRANSFORM_FUNCTIONS = { matrix: matrix, matrix3d: matrix3d }; var DEFAULT_VALUE = { type: 16 /* PERCENTAGE_TOKEN */, number: 50, flags: FLAG_INTEGER }; var DEFAULT = [DEFAULT_VALUE, DEFAULT_VALUE]; var transformOrigin = { name: 'transform-origin', initialValue: '50% 50%', prefix: true, type: 1 /* LIST */, parse: function (_context, tokens) { var origins = tokens.filter(isLengthPercentage); if (origins.length !== 2) { return DEFAULT; } return [origins[0], origins[1]]; } }; var visibility = { name: 'visible', initialValue: 'none', prefix: false, type: 2 /* IDENT_VALUE */, parse: function (_context, visibility) { switch (visibility) { case 'hidden': return 1 /* HIDDEN */; case 'collapse': return 2 /* COLLAPSE */; case 'visible': default: return 0 /* VISIBLE */; } } }; var WORD_BREAK; (function (WORD_BREAK) { WORD_BREAK["NORMAL"] = "normal"; WORD_BREAK["BREAK_ALL"] = "break-all"; WORD_BREAK["KEEP_ALL"] = "keep-all"; })(WORD_BREAK || (WORD_BREAK = {})); var wordBreak = { name: 'word-break', initialValue: 'normal', prefix: false, type: 2 /* IDENT_VALUE */, parse: function (_context, wordBreak) { switch (wordBreak) { case 'break-all': return WORD_BREAK.BREAK_ALL; case 'keep-all': return WORD_BREAK.KEEP_ALL; case 'normal': default: return WORD_BREAK.NORMAL; } } }; var zIndex = { name: 'z-index', initialValue: 'auto', prefix: false, type: 0 /* VALUE */, parse: function (_context, token) { if (token.type === 20 /* IDENT_TOKEN */) { return { auto: true, order: 0 }; } if (isNumberToken(token)) { return { auto: false, order: token.number }; } throw new Error("Invalid z-index number parsed"); } }; var time = { name: 'time', parse: function (_context, value) { if (value.type === 15 /* DIMENSION_TOKEN */) { switch (value.unit.toLowerCase()) { case 's': return 1000 * value.number; case 'ms': return value.number; } } throw new Error("Unsupported time type"); } }; var opacity = { name: 'opacity', initialValue: '1', type: 0 /* VALUE */, prefix: false, parse: function (_context, token) { if (isNumberToken(token)) { return token.number; } return 1; } }; var textDecorationColor = { name: "text-decoration-color", initialValue: 'transparent', prefix: false, type: 3 /* TYPE_VALUE */, format: 'color' }; var textDecorationLine = { name: 'text-decoration-line', initialValue: 'none', prefix: false, type: 1 /* LIST */, parse: function (_context, tokens) { return tokens .filter(isIdentToken) .map(function (token) { switch (token.value) { case 'underline': return 1 /* UNDERLINE */; case 'overline': return 2 /* OVERLINE */; case 'line-through': return 3 /* LINE_THROUGH */; case 'none': return 4 /* BLINK */; } return 0 /* NONE */; }) .filter(function (line) { return line !== 0 /* NONE */; }); } }; var fontFamily = { name: "font-family", initialValue: '', prefix: false, type: 1 /* LIST */, parse: function (_context, tokens) { var accumulator = []; var results = []; tokens.forEach(function (token) { switch (token.type) { case 20 /* IDENT_TOKEN */: case 0 /* STRING_TOKEN */: accumulator.push(token.value); break; case 17 /* NUMBER_TOKEN */: accumulator.push(token.number.toString()); break; case 4 /* COMMA_TOKEN */: results.push(accumulator.join(' ')); accumulator.length = 0; break; } }); if (accumulator.length) { results.push(accumulator.join(' ')); } return results.map(function (result) { return (result.indexOf(' ') === -1 ? result : "'" + result + "'"); }); } }; var fontSize = { name: "font-size", initialValue: '0', prefix: false, type: 3 /* TYPE_VALUE */, format: 'length' }; var fontWeight = { name: 'font-weight', initialValue: 'normal', type: 0 /* VALUE */, prefix: false, parse: function (_context, token) { if (isNumberToken(token)) { return token.number; } if (isIdentToken(token)) { switch (token.value) { case 'bold': return 700; case 'normal': default: return 400; } } return 400; } }; var fontVariant = { name: 'font-variant', initialValue: 'none', type: 1 /* LIST */, prefix: false, parse: function (_context, tokens) { return tokens.filter(isIdentToken).map(function (token) { return token.value; }); } }; var fontStyle = { name: 'font-style', initialValue: 'normal', prefix: false, type: 2 /* IDENT_VALUE */, parse: function (_context, overflow) { switch (overflow) { case 'oblique': return "oblique" /* OBLIQUE */; case 'italic': return "italic" /* ITALIC */; case 'normal': default: return "normal" /* NORMAL */; } } }; var contains = function (bit, value) { return (bit & value) !== 0; }; var content = { name: 'content', initialValue: 'none', type: 1 /* LIST */, prefix: false, parse: function (_context, tokens) { if (tokens.length === 0) { return []; } var first = tokens[0]; if (first.type === 20 /* IDENT_TOKEN */ && first.value === 'none') { return []; } return tokens; } }; var counterIncrement = { name: 'counter-increment', initialValue: 'none', prefix: true, type: 1 /* LIST */, parse: function (_context, tokens) { if (tokens.length === 0) { return null; } var first = tokens[0]; if (first.type === 20 /* IDENT_TOKEN */ && first.value === 'none') { return null; } var increments = []; var filtered = tokens.filter(nonWhiteSpace); for (var i = 0; i < filtered.length; i++) { var counter = filtered[i]; var next = filtered[i + 1]; if (counter.type === 20 /* IDENT_TOKEN */) { var increment = next && isNumberToken(next) ? next.number : 1; increments.push({ counter: counter.value, increment: increment }); } } return increments; } }; var counterReset = { name: 'counter-reset', initialValue: 'none', prefix: true, type: 1 /* LIST */, parse: function (_context, tokens) { if (tokens.length === 0) { return []; } var resets = []; var filtered = tokens.filter(nonWhiteSpace); for (var i = 0; i < filtered.length; i++) { var counter = filtered[i]; var next = filtered[i + 1]; if (isIdentToken(counter) && counter.value !== 'none') { var reset = next && isNumberToken(next) ? next.number : 0; resets.push({ counter: counter.value, reset: reset }); } } return resets; } }; var duration = { name: 'duration', initialValue: '0s', prefix: false, type: 1 /* LIST */, parse: function (context, tokens) { return tokens.filter(isDimensionToken).map(function (token) { return time.parse(context, token); }); } }; var quotes = { name: 'quotes', initialValue: 'none', prefix: true, type: 1 /* LIST */, parse: function (_context, tokens) { if (tokens.length === 0) { return null; } var first = tokens[0]; if (first.type === 20 /* IDENT_TOKEN */ && first.value === 'none') { return null; } var quotes = []; var filtered = tokens.filter(isStringToken); if (filtered.length % 2 !== 0) { return null; } for (var i = 0; i < filtered.length; i += 2) { var open_1 = filtered[i].value; var close_1 = filtered[i + 1].value; quotes.push({ open: open_1, close: close_1 }); } return quotes; } }; var getQuote = function (quotes, depth, open) { if (!quotes) { return ''; } var quote = quotes[Math.min(depth, quotes.length - 1)]; if (!quote) { return ''; } return open ? quote.open : quote.close; }; var boxShadow = { name: 'box-shadow', initialValue: 'none', type: 1 /* LIST */, prefix: false, parse: function (context, tokens) { if (tokens.length === 1 && isIdentWithValue(tokens[0], 'none')) { return []; } return parseFunctionArgs(tokens).map(function (values) { var shadow = { color: 0x000000ff, offsetX: ZERO_LENGTH, offsetY: ZERO_LENGTH, blur: ZERO_LENGTH, spread: ZERO_LENGTH, inset: false }; var c = 0; for (var i = 0; i < values.length; i++) { var token = values[i]; if (isIdentWithValue(token, 'inset')) { shadow.inset = true; } else if (isLength(token)) { if (c === 0) { shadow.offsetX = token; } else if (c === 1) { shadow.offsetY = token; } else if (c === 2) { shadow.blur = token; } else { shadow.spread = token; } c++; } else { shadow.color = color$1.parse(context, token); } } return shadow; }); } }; var paintOrder = { name: 'paint-order', initialValue: 'normal', prefix: false, type: 1 /* LIST */, parse: function (_context, tokens) { var DEFAULT_VALUE = [0 /* FILL */, 1 /* STROKE */, 2 /* MARKERS */]; var layers = []; tokens.filter(isIdentToken).forEach(function (token) { switch (token.value) { case 'stroke': layers.push(1 /* STROKE */); break; case 'fill': layers.push(0 /* FILL */); break; case 'markers': layers.push(2 /* MARKERS */); break; } }); DEFAULT_VALUE.forEach(function (value) { if (layers.indexOf(value) === -1) { layers.push(value); } }); return layers; } }; var webkitTextStrokeColor = { name: "-webkit-text-stroke-color", initialValue: 'currentcolor', prefix: false, type: 3 /* TYPE_VALUE */, format: 'color' }; var webkitTextStrokeWidth = { name: "-webkit-text-stroke-width", initialValue: '0', type: 0 /* VALUE */, prefix: false, parse: function (_context, token) { if (isDimensionToken(token)) { return token.number; } return 0; } }; var CSSParsedDeclaration = /** @class */ (function () { function CSSParsedDeclaration(context, declaration) { var _a, _b; this.animationDuration = parse$4(context, duration, declaration.animationDuration); this.backgroundClip = parse$4(context, backgroundClip, declaration.backgroundClip); this.backgroundColor = parse$4(context, backgroundColor, declaration.backgroundColor); this.backgroundImage = parse$4(context, backgroundImage, declaration.backgroundImage); this.backgroundOrigin = parse$4(context, backgroundOrigin, declaration.backgroundOrigin); this.backgroundPosition = parse$4(context, backgroundPosition, declaration.backgroundPosition); this.backgroundRepeat = parse$4(context, backgroundRepeat, declaration.backgroundRepeat); this.backgroundSize = parse$4(context, backgroundSize, declaration.backgroundSize); this.borderTopColor = parse$4(context, borderTopColor, declaration.borderTopColor); this.borderRightColor = parse$4(context, borderRightColor, declaration.borderRightColor); this.borderBottomColor = parse$4(context, borderBottomColor, declaration.borderBottomColor); this.borderLeftColor = parse$4(context, borderLeftColor, declaration.borderLeftColor); this.borderTopLeftRadius = parse$4(context, borderTopLeftRadius, declaration.borderTopLeftRadius); this.borderTopRightRadius = parse$4(context, borderTopRightRadius, declaration.borderTopRightRadius); this.borderBottomRightRadius = parse$4(context, borderBottomRightRadius, declaration.borderBottomRightRadius); this.borderBottomLeftRadius = parse$4(context, borderBottomLeftRadius, declaration.borderBottomLeftRadius); this.borderTopStyle = parse$4(context, borderTopStyle, declaration.borderTopStyle); this.borderRightStyle = parse$4(context, borderRightStyle, declaration.borderRightStyle); this.borderBottomStyle = parse$4(context, borderBottomStyle, declaration.borderBottomStyle); this.borderLeftStyle = parse$4(context, borderLeftStyle, declaration.borderLeftStyle); this.borderTopWidth = parse$4(context, borderTopWidth, declaration.borderTopWidth); this.borderRightWidth = parse$4(context, borderRightWidth, declaration.borderRightWidth); this.borderBottomWidth = parse$4(context, borderBottomWidth, declaration.borderBottomWidth); this.borderLeftWidth = parse$4(context, borderLeftWidth, declaration.borderLeftWidth); this.boxShadow = parse$4(context, boxShadow, declaration.boxShadow); this.color = parse$4(context, color, declaration.color); this.direction = parse$4(context, direction, declaration.direction); this.display = parse$4(context, display, declaration.display); this.float = parse$4(context, float, declaration.cssFloat); this.fontFamily = parse$4(context, fontFamily, declaration.fontFamily); this.fontSize = parse$4(context, fontSize, declaration.fontSize); this.fontStyle = parse$4(context, fontStyle, declaration.fontStyle); this.fontVariant = parse$4(context, fontVariant, declaration.fontVariant); this.fontWeight = parse$4(context, fontWeight, declaration.fontWeight); this.letterSpacing = parse$4(context, letterSpacing, declaration.letterSpacing); this.lineBreak = parse$4(context, lineBreak, declaration.lineBreak); this.lineHeight = parse$4(context, lineHeight, declaration.lineHeight); this.listStyleImage = parse$4(context, listStyleImage, declaration.listStyleImage); this.listStylePosition = parse$4(context, listStylePosition, declaration.listStylePosition); this.listStyleType = parse$4(context, listStyleType, declaration.listStyleType); this.marginTop = parse$4(context, marginTop, declaration.marginTop); this.marginRight = parse$4(context, marginRight, declaration.marginRight); this.marginBottom = parse$4(context, marginBottom, declaration.marginBottom); this.marginLeft = parse$4(context, marginLeft, declaration.marginLeft); this.opacity = parse$4(context, opacity, declaration.opacity); var overflowTuple = parse$4(context, overflow, declaration.overflow); this.overflowX = overflowTuple[0]; this.overflowY = overflowTuple[overflowTuple.length > 1 ? 1 : 0]; this.overflowWrap = parse$4(context, overflowWrap, declaration.overflowWrap); this.paddingTop = parse$4(context, paddingTop, declaration.paddingTop); this.paddingRight = parse$4(context, paddingRight, declaration.paddingRight); this.paddingBottom = parse$4(context, paddingBottom, declaration.paddingBottom); this.paddingLeft = parse$4(context, paddingLeft, declaration.paddingLeft); this.paintOrder = parse$4(context, paintOrder, declaration.paintOrder); this.position = parse$4(context, position, declaration.position); this.textAlign = parse$4(context, textAlign, declaration.textAlign); this.textDecorationColor = parse$4(context, textDecorationColor, (_a = declaration.textDecorationColor) !== null && _a !== void 0 ? _a : declaration.color); this.textDecorationLine = parse$4(context, textDecorationLine, (_b = declaration.textDecorationLine) !== null && _b !== void 0 ? _b : declaration.textDecoration); this.textShadow = parse$4(context, textShadow, declaration.textShadow); this.textTransform = parse$4(context, textTransform, declaration.textTransform); this.transform = parse$4(context, transform$1, declaration.transform); this.transformOrigin = parse$4(context, transformOrigin, declaration.transformOrigin); this.visibility = parse$4(context, visibility, declaration.visibility); this.webkitTextStrokeColor = parse$4(context, webkitTextStrokeColor, declaration.webkitTextStrokeColor); this.webkitTextStrokeWidth = parse$4(context, webkitTextStrokeWidth, declaration.webkitTextStrokeWidth); this.wordBreak = parse$4(context, wordBreak, declaration.wordBreak); this.zIndex = parse$4(context, zIndex, declaration.zIndex); } CSSParsedDeclaration.prototype.isVisible = function () { return this.display > 0 && this.opacity > 0 && this.visibility === 0 /* VISIBLE */; }; CSSParsedDeclaration.prototype.isTransparent = function () { return isTransparent(this.backgroundColor); }; CSSParsedDeclaration.prototype.isTransformed = function () { return this.transform !== null; }; CSSParsedDeclaration.prototype.isPositioned = function () { return this.position !== 0 /* STATIC */; }; CSSParsedDeclaration.prototype.isPositionedWithZIndex = function () { return this.isPositioned() && !this.zIndex.auto; }; CSSParsedDeclaration.prototype.isFloating = function () { return this.float !== 0 /* NONE */; }; CSSParsedDeclaration.prototype.isInlineLevel = function () { return (contains(this.display, 4 /* INLINE */) || contains(this.display, 33554432 /* INLINE_BLOCK */) || contains(this.display, 268435456 /* INLINE_FLEX */) || contains(this.display, 536870912 /* INLINE_GRID */) || contains(this.display, 67108864 /* INLINE_LIST_ITEM */) || contains(this.display, 134217728 /* INLINE_TABLE */)); }; return CSSParsedDeclaration; }()); var CSSParsedPseudoDeclaration = /** @class */ (function () { function CSSParsedPseudoDeclaration(context, declaration) { this.content = parse$4(context, content, declaration.content); this.quotes = parse$4(context, quotes, declaration.quotes); } return CSSParsedPseudoDeclaration; }()); var CSSParsedCounterDeclaration = /** @class */ (function () { function CSSParsedCounterDeclaration(context, declaration) { this.counterIncrement = parse$4(context, counterIncrement, declaration.counterIncrement); this.counterReset = parse$4(context, counterReset, declaration.counterReset); } return CSSParsedCounterDeclaration; }()); // eslint-disable-next-line @typescript-eslint/no-explicit-any var parse$4 = function (context, descriptor, style) { var tokenizer = new Tokenizer(); var value = style !== null && typeof style !== 'undefined' ? style.toString() : descriptor.initialValue; tokenizer.write(value); var parser = new Parser(tokenizer.read()); switch (descriptor.type) { case 2 /* IDENT_VALUE */: var token = parser.parseComponentValue(); return descriptor.parse(context, isIdentToken(token) ? token.value : descriptor.initialValue); case 0 /* VALUE */: return descriptor.parse(context, parser.parseComponentValue()); case 1 /* LIST */: return descriptor.parse(context, parser.parseComponentValues()); case 4 /* TOKEN_VALUE */: return parser.parseComponentValue(); case 3 /* TYPE_VALUE */: switch (descriptor.format) { case 'angle': return angle$1.parse(context, parser.parseComponentValue()); case 'color': return color$1.parse(context, parser.parseComponentValue()); case 'image': return image$1.parse(context, parser.parseComponentValue()); case 'length': var length_1 = parser.parseComponentValue(); return isLength(length_1) ? length_1 : ZERO_LENGTH; case 'length-percentage': var value_1 = parser.parseComponentValue(); return isLengthPercentage(value_1) ? value_1 : ZERO_LENGTH; case 'time': return time.parse(context, parser.parseComponentValue()); } break; } }; var elementDebuggerAttribute = 'data-html2canvas-debug'; var getElementDebugType = function (element) { var attribute = element.getAttribute(elementDebuggerAttribute); switch (attribute) { case 'all': return 1 /* ALL */; case 'clone': return 2 /* CLONE */; case 'parse': return 3 /* PARSE */; case 'render': return 4 /* RENDER */; default: return 0 /* NONE */; } }; var isDebugging = function (element, type) { var elementType = getElementDebugType(element); return elementType === 1 /* ALL */ || type === elementType; }; var ElementContainer = /** @class */ (function () { function ElementContainer(context, element) { this.context = context; this.textNodes = []; this.elements = []; this.flags = 0; if (isDebugging(element, 3 /* PARSE */)) { debugger; } this.styles = new CSSParsedDeclaration(context, window.getComputedStyle(element, null)); if (isHTMLElementNode(element)) { if (this.styles.animationDuration.some(function (duration) { return duration > 0; })) { element.style.animationDuration = '0s'; } if (this.styles.transform !== null) { // getBoundingClientRect takes transforms into account element.style.transform = 'none'; } } this.bounds = parseBounds(this.context, element); if (isDebugging(element, 4 /* RENDER */)) { this.flags |= 16 /* DEBUG_RENDER */; } } return ElementContainer; }()); /* * text-segmentation 1.0.3 * Copyright (c) 2022 Niklas von Hertzen * Released under MIT License */ var base64 = 'AAAAAAAAAAAAEA4AGBkAAFAaAAACAAAAAAAIABAAGAAwADgACAAQAAgAEAAIABAACAAQAAgAEAAIABAACAAQAAgAEAAIABAAQABIAEQATAAIABAACAAQAAgAEAAIABAAVABcAAgAEAAIABAACAAQAGAAaABwAHgAgACIAI4AlgAIABAAmwCjAKgAsAC2AL4AvQDFAMoA0gBPAVYBWgEIAAgACACMANoAYgFkAWwBdAF8AX0BhQGNAZUBlgGeAaMBlQGWAasBswF8AbsBwwF0AcsBYwHTAQgA2wG/AOMBdAF8AekB8QF0AfkB+wHiAHQBfAEIAAMC5gQIAAsCEgIIAAgAFgIeAggAIgIpAggAMQI5AkACygEIAAgASAJQAlgCYAIIAAgACAAKBQoFCgUTBRMFGQUrBSsFCAAIAAgACAAIAAgACAAIAAgACABdAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACABoAmgCrwGvAQgAbgJ2AggAHgEIAAgACADnAXsCCAAIAAgAgwIIAAgACAAIAAgACACKAggAkQKZAggAPADJAAgAoQKkAqwCsgK6AsICCADJAggA0AIIAAgACAAIANYC3gIIAAgACAAIAAgACABAAOYCCAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAkASoB+QIEAAgACAA8AEMCCABCBQgACABJBVAFCAAIAAgACAAIAAgACAAIAAgACABTBVoFCAAIAFoFCABfBWUFCAAIAAgACAAIAAgAbQUIAAgACAAIAAgACABzBXsFfQWFBYoFigWKBZEFigWKBYoFmAWfBaYFrgWxBbkFCAAIAAgACAAIAAgACAAIAAgACAAIAMEFCAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAMgFCADQBQgACAAIAAgACAAIAAgACAAIAAgACAAIAO4CCAAIAAgAiQAIAAgACABAAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAD0AggACAD8AggACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIANYFCAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAMDvwAIAAgAJAIIAAgACAAIAAgACAAIAAgACwMTAwgACAB9BOsEGwMjAwgAKwMyAwsFYgE3A/MEPwMIAEUDTQNRAwgAWQOsAGEDCAAIAAgACAAIAAgACABpAzQFNQU2BTcFOAU5BToFNAU1BTYFNwU4BTkFOgU0BTUFNgU3BTgFOQU6BTQFNQU2BTcFOAU5BToFNAU1BTYFNwU4BTkFOgU0BTUFNgU3BTgFOQU6BTQFNQU2BTcFOAU5BToFNAU1BTYFNwU4BTkFOgU0BTUFNgU3BTgFOQU6BTQFNQU2BTcFOAU5BToFNAU1BTYFNwU4BTkFOgU0BTUFNgU3BTgFOQU6BTQFNQU2BTcFOAU5BToFNAU1BTYFNwU4BTkFOgU0BTUFNgU3BTgFOQU6BTQFNQU2BTcFOAU5BToFNAU1BTYFNwU4BTkFOgU0BTUFNgU3BTgFOQU6BTQFNQU2BTcFOAU5BToFNAU1BTYFNwU4BTkFOgU0BTUFNgU3BTgFOQU6BTQFNQU2BTcFOAU5BToFNAU1BTYFNwU4BTkFOgU0BTUFNgU3BTgFOQU6BTQFNQU2BTcFOAU5BToFNAU1BTYFNwU4BTkFOgU0BTUFNgU3BTgFOQU6BTQFNQU2BTcFOAU5BToFNAU1BTYFNwU4BTkFOgU0BTUFNgU3BTgFOQU6BTQFNQU2BTcFOAU5BToFNAU1BTYFNwU4BTkFOgU0BTUFNgU3BTgFOQU6BTQFNQU2BTcFOAU5BToFNAU1BTYFNwU4BTkFOgU0BTUFNgU3BTgFOQU6BTQFNQU2BTcFOAU5BToFNAU1BTYFNwU4BTkFOgU0BTUFNgU3BTgFOQU6BTQFNQU2BTcFOAU5BToFNAU1BTYFNwU4BTkFOgU0BTUFNgU3BTgFOQU6BTQFNQU2BTcFOAU5BToFNAU1BTYFNwU4BTkFOgU0BTUFNgU3BTgFOQU6BTQFNQU2BTcFOAU5BToFNAU1BTYFNwU4BTkFOgU0BTUFNgU3BTgFOQU6BTQFNQU2BTcFOAU5BToFNAU1BTYFNwU4BTkFIQUoBSwFCAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACABtAwgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACABMAEwACAAIAAgACAAIABgACAAIAAgACAC/AAgACAAyAQgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACACAAIAAwAAgACAAIAAgACAAIAAgACAAIAAAARABIAAgACAAIABQASAAIAAgAIABwAEAAjgCIABsAqAC2AL0AigDQAtwC+IJIQqVAZUBWQqVAZUBlQGVAZUBlQGrC5UBlQGVAZUBlQGVAZUBlQGVAXsKlQGVAbAK6wsrDGUMpQzlDJUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAfAKAAuZA64AtwCJALoC6ADwAAgAuACgA/oEpgO6AqsD+AAIAAgAswMIAAgACAAIAIkAuwP5AfsBwwPLAwgACAAIAAgACADRA9kDCAAIAOED6QMIAAgACAAIAAgACADuA/YDCAAIAP4DyQAIAAgABgQIAAgAXQAOBAgACAAIAAgACAAIABMECAAIAAgACAAIAAgACAD8AAQBCAAIAAgAGgQiBCoECAExBAgAEAEIAAgACAAIAAgACAAIAAgACAAIAAgACAA4BAgACABABEYECAAIAAgATAQYAQgAVAQIAAgACAAIAAgACAAIAAgACAAIAFoECAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgAOQEIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAB+BAcACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAEABhgSMBAgACAAIAAgAlAQIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAwAEAAQABAADAAMAAwADAAQABAAEAAQABAAEAAQABHATAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgAdQMIAAgACAAIAAgACAAIAMkACAAIAAgAfQMIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACACFA4kDCAAIAAgACAAIAOcBCAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAIcDCAAIAAgACAAIAAgACAAIAAgACAAIAJEDCAAIAAgACADFAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACABgBAgAZgQIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgAbAQCBXIECAAIAHkECAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACABAAJwEQACjBKoEsgQIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAC6BMIECAAIAAgACAAIAAgACABmBAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgAxwQIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAGYECAAIAAgAzgQIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgAigWKBYoFigWKBYoFigWKBd0FXwUIAOIF6gXxBYoF3gT5BQAGCAaKBYoFigWKBYoFigWKBYoFigWKBYoFigXWBIoFigWKBYoFigWKBYoFigWKBYsFEAaKBYoFigWKBYoFigWKBRQGCACKBYoFigWKBQgACAAIANEECAAIABgGigUgBggAJgYIAC4GMwaKBYoF0wQ3Bj4GigWKBYoFigWKBYoFigWKBYoFigWKBYoFigUIAAgACAAIAAgACAAIAAgAigWKBYoFigWKBYoFigWKBYoFigWKBYoFigWKBYoFigWKBYoFigWKBYoFigWKBYoFigWKBYoFigWKBYoFigWLBf///////wQABAAEAAQABAAEAAQABAAEAAQAAwAEAAQAAgAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAAAAAAAAAAAAAAAAAAAAAAAAAOAAAAAAAAAAQADgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAUABQAFAAUABQAFAAUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAAAAUAAAAFAAUAAAAFAAUAAAAFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAEAAQABAAEAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAUABQAFAAUABQAFAAUABQAFAAUABQAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAUABQAFAAUABQAFAAUAAQAAAAUABQAFAAUABQAFAAAAAAAFAAUAAAAFAAUABQAFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFAAUABQAFAAUABQAFAAUABQAFAAUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFAAUABQAFAAUABQAFAAUABQAAAAAAAAAAAAAAAAAAAAAAAAAFAAAAAAAFAAUAAQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABwAFAAUABQAFAAAABwAHAAcAAAAHAAcABwAFAAEAAAAAAAAAAAAAAAAAAAAAAAUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHAAcABwAFAAUABQAFAAcABwAFAAUAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHAAAAAQABAAAAAAAAAAAAAAAFAAUABQAFAAAABwAFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABQAHAAcABwAHAAcAAAAHAAcAAAAAAAUABQAHAAUAAQAHAAEABwAFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABQAFAAUABQAFAAUABwABAAUABQAFAAUAAAAAAAAAAAAAAAEAAQABAAEAAQABAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABwAFAAUAAAAAAAAAAAAAAAAABQAFAAUABQAFAAUAAQAFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAUABQAFAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQABQANAAQABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQABAAEAAQABAAEAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAEAAQABAAEAAQABAAEAAQABAAEAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAQABAAEAAQABAAEAAQABAAAAAAAAAAAAAAAAAAAAAAABQAHAAUABQAFAAAAAAAAAAcABQAFAAUABQAFAAQABAAEAAQABAAEAAQABAAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAUABQAFAAUAAAAFAAUABQAFAAUAAAAFAAUABQAAAAUABQAFAAUABQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFAAUABQAAAAAAAAAAAAUABQAFAAcAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABQAHAAUAAAAHAAcABwAFAAUABQAFAAUABQAFAAUABwAHAAcABwAFAAcABwAAAAUABQAFAAUABQAFAAUAAAAAAAAAAAAAAAAAAAAAAAAAAAAFAAUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAUABwAHAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABQAAAAUABwAHAAUABQAFAAUAAAAAAAcABwAAAAAABwAHAAUAAAAAAAAAAAAAAAAAAAAAAAAABQAAAAAAAAAAAAAAAAAAAAAAAAAAAAUABQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABQAAAAAABQAFAAcAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFAAAABwAHAAcABQAFAAAAAAAAAAAABQAFAAAAAAAFAAUABQAAAAAAAAAFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAUABQAAAAAAAAAFAAAAAAAAAAAAAAAAAAAAAAAAAAAABwAFAAUABQAFAAUAAAAFAAUABwAAAAcABwAFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFAAUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFAAUABQAFAAUABQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAUAAAAFAAUABwAFAAUABQAFAAAAAAAHAAcAAAAAAAcABwAFAAAAAAAAAAAAAAAAAAAABQAFAAUAAAAAAAAAAAAAAAAAAAAAAAAAAAAFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFAAcABwAAAAAAAAAHAAcABwAAAAcABwAHAAUAAAAAAAAAAAAAAAAAAAAAAAAABQAAAAAAAAAAAAAAAAAAAAAABQAHAAcABwAFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAUABwAHAAcABwAAAAUABQAFAAAABQAFAAUABQAAAAAAAAAAAAAAAAAAAAUABQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABQAAAAcABQAHAAcABQAHAAcAAAAFAAcABwAAAAcABwAFAAUAAAAAAAAAAAAAAAAAAAAFAAUAAAAAAAAAAAAAAAAAAAAAAAAABQAFAAcABwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAUABQAAAAUABwAAAAAAAAAAAAAAAAAAAAAAAAAAAAUAAAAAAAAAAAAFAAcABwAFAAUABQAAAAUAAAAHAAcABwAHAAcABwAHAAUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAUAAAAHAAUABQAFAAUABQAFAAUAAAAAAAAAAAAAAAAAAAAAAAUABQAFAAUABQAFAAUABQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFAAAABwAFAAUABQAFAAUABQAFAAUABQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABQAFAAUABQAFAAUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAUABQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABQAAAAUAAAAFAAAAAAAAAAAABwAHAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABwAFAAUABQAFAAUAAAAFAAUAAAAAAAAAAAAAAAUABQAFAAUABQAFAAUABQAFAAUABQAAAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAUABQAFAAUABwAFAAUABQAFAAUABQAAAAUABQAHAAcABQAFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHAAcABQAFAAAAAAAAAAAABQAFAAUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFAAUABQAFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABQAAAAcABQAFAAAAAAAAAAAAAAAAAAUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABQAFAAUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAUABQAHAAUABQAFAAUABQAFAAUABwAHAAcABwAHAAcABwAHAAUABwAHAAUABQAFAAUABQAFAAUABQAFAAUABQAAAAAAAAAAAAAAAAAAAAAAAAAFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABQAFAAUABwAHAAcABwAFAAUABwAHAAcAAAAAAAAAAAAHAAcABQAHAAcABwAHAAcABwAFAAUABQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABQAFAAcABwAFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAcABQAHAAUABQAFAAUABQAFAAUAAAAFAAAABQAAAAAABQAFAAUABQAFAAUABQAFAAcABwAHAAcABwAHAAUABQAFAAUABQAFAAUABQAFAAUAAAAAAAUABQAFAAUABQAHAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAUABQAFAAUABQAFAAUABwAFAAcABwAHAAcABwAFAAcABwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFAAUABQAFAAUABQAFAAUABQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFAAUABwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHAAUABQAFAAUABwAHAAUABQAHAAUABQAFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFAAcABQAFAAcABwAHAAUABwAFAAUABQAHAAcAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABwAHAAcABwAHAAcABwAHAAUABQAFAAUABQAFAAUABQAHAAcABQAFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABQAFAAUAAAAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAcABQAFAAUABQAFAAUABQAAAAAAAAAAAAUAAAAAAAAAAAAAAAAABQAAAAAABwAFAAUAAAAAAAAAAAAAAAAABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAAABQAFAAUABQAFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAUABQAFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABQAFAAUABQAFAAUADgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAUABQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAUABQAFAAUAAAAFAAUABQAFAAUABQAFAAUABQAFAAAAAAAAAAAABQAAAAAAAAAFAAAAAAAAAAAABQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABwAHAAUABQAHAAAAAAAAAAAABQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAcABwAHAAcABQAFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAUAAAAAAAAAAAAAAAAABQAFAAUABQAFAAUABQAFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAUABQAFAAUABQAFAAUABQAFAAUABQAHAAcAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFAAcABwAFAAUABQAFAAcABwAFAAUABwAHAAAAAAAAAAAAAAAFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAUABQAFAAUABQAFAAcABwAFAAUABwAHAAUABQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFAAAAAAAAAAAAAAAAAAAAAAAFAAcAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAUAAAAFAAUABQAAAAAABQAFAAAAAAAAAAAAAAAFAAUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAcABQAFAAcABwAAAAAAAAAAAAAABwAFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAcABwAFAAcABwAFAAcABwAAAAcABQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAUABQAFAAUABQAAAAAAAAAAAAAAAAAFAAUABQAAAAUABQAAAAAAAAAAAAAABQAFAAUABQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFAAUABQAAAAAAAAAAAAUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAUABQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAcABQAHAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFAAUABQAFAAUABwAFAAUABQAFAAUABQAFAAUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABwAHAAcABQAFAAUABQAFAAUABQAFAAUABwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHAAcABwAFAAUABQAHAAcABQAHAAUABQAAAAAAAAAAAAAAAAAFAAAABwAHAAcABQAFAAUABQAFAAUABQAFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAUABwAHAAcABwAAAAAABwAHAAAAAAAHAAcABwAAAAAAAAAAAAAAAAAAAAAAAAAFAAAAAAAAAAAAAAAAAAAAAAAAAAAABwAHAAAAAAAFAAUABQAFAAUABQAFAAAAAAAAAAUABQAFAAUABQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHAAcABwAFAAUABQAFAAUABQAFAAUABwAHAAUABQAFAAcABQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABQAHAAcABQAFAAUABQAFAAUABwAFAAcABwAFAAcABQAFAAcABQAFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABQAHAAcABQAFAAUABQAAAAAABwAHAAcABwAFAAUABwAFAAUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABQAFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAcABwAHAAUABQAFAAUABQAFAAUABQAHAAcABQAHAAUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAUABwAFAAcABwAFAAUABQAFAAUABQAHAAUAAAAAAAAAAAAAAAAAAAAAAAcABwAFAAUABQAFAAcABQAFAAUABQAFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHAAcABwAFAAUABQAFAAUABQAFAAUABQAHAAUABQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHAAcABwAFAAUABQAFAAAAAAAFAAUABwAHAAcABwAFAAAAAAAAAAcAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFAAUABQAFAAUABQAFAAUABQAFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAUAAAAAAAAAAAAAAAAAAAAAAAAABQAFAAUABQAFAAUABwAHAAUABQAFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAcABQAFAAUABQAFAAUABQAAAAUABQAFAAUABQAFAAcABQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUAAAAHAAUABQAFAAUABQAFAAUABwAFAAUABwAFAAUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABQAFAAUABQAFAAUAAAAAAAAABQAAAAUABQAAAAUAAAAAAAAAAAAAAAAAAAAAAAAAAAAHAAcABwAHAAcAAAAFAAUAAAAHAAcABQAHAAUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFAAUABwAHAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFAAUABQAFAAUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFAAUABQAFAAUABQAFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABQAAAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAAAAAAAAAAAAAAAAAAABQAFAAUABQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAcABwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAUABQAAAAUABQAFAAAAAAAFAAUABQAFAAUABQAFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABQAFAAUABQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABQAFAAUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAAAAAAAAAAABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAAAAAAAAAAAAAAAAAAAAAAFAAAAAAAAAAAAAAAAAAAAAAAAAAAABQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAUABQAFAAUABQAAAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABQAFAAUABQAFAAUABQAAAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAAAAAABQAFAAUABQAFAAUABQAAAAUABQAAAAUABQAFAAUABQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFAAUABQAFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABQAFAAUABQAFAAUABQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOAA4ADgAOAA4ADgAOAA4ADgAOAA4ADgAOAA4ADgAOAA4ADgAOAA4ADgAOAA4ADgAOAA4ADgAFAAUABQAFAAUADgAOAA4ADgAOAA4ADwAPAA8ADwAPAA8ADwAPAA8ADwAPAA8ADwAPAA8ADwAPAA8ADwAPAA8ADwAPAA8ADwAPAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAcABwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABwAHAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAAAAAAAAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAKAAoACgAKAAoACgAKAAoACgAKAAoACgAKAAoACgAKAAoACgAKAAoACgAKAAoACgAMAAwADAAMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkAAAAAAAAAAAAKAAoACgAKAAoACgAKAAoACgAKAAoACgAKAAoACgAKAAoACgAKAAoACgAKAAoACgAKAAoACgAKAAoACgAKAAoACgAAAAAAAAAAAAsADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwACwAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAAAAAADgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOAA4ADgAOAA4ADgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADgAOAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA4ADgAAAAAAAAAAAAAAAAAAAAAADgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADgAOAA4ADgAOAA4ADgAOAA4ADgAOAAAAAAAAAAAADgAOAA4AAAAAAAAAAAAAAAAAAAAOAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADgAOAAAAAAAAAAAAAAAAAAAAAAAAAAAADgAAAAAAAAAAAAAAAAAAAAAAAAAOAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADgAOAA4ADgAAAA4ADgAOAA4ADgAOAAAADgAOAA4ADgAOAA4ADgAOAA4ADgAOAA4AAAAOAA4ADgAOAA4ADgAOAA4ADgAOAA4ADgAOAA4ADgAOAA4ADgAOAA4ADgAOAA4ADgAOAA4ADgAOAA4ADgAOAA4ADgAOAAAAAAAAAAAAAAAAAAAAAAAAAAAADgAOAA4ADgAOAA4ADgAOAA4ADgAOAA4ADgAOAA4ADgAOAA4AAAAAAA4ADgAOAA4ADgAOAA4ADgAOAA4ADgAAAA4AAAAOAAAAAAAAAAAAAAAAAA4AAAAAAAAAAAAAAAAADgAAAAAAAAAAAAAAAAAAAAAAAAAAAA4ADgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADgAAAAAADgAAAAAAAAAAAA4AAAAOAAAAAAAAAAAADgAOAA4AAAAOAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOAA4ADgAOAA4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOAA4ADgAAAAAAAAAAAAAAAAAAAAAAAAAOAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOAA4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA4ADgAOAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADgAOAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADgAAAAAAAAAAAA4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOAAAADgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOAA4ADgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA4ADgAOAA4ADgAOAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA4ADgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADgAAAAAADgAOAA4ADgAOAA4ADgAOAA4ADgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADgAOAA4ADgAOAA4ADgAOAA4ADgAOAA4ADgAOAA4ADgAOAA4ADgAAAA4ADgAOAA4ADgAOAA4ADgAOAA4ADgAOAA4ADgAOAAAAAAAAAAAAAAAAAAAAAAAAAAAADgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA4AAAAAAA4ADgAOAA4ADgAOAA4ADgAOAAAADgAOAA4ADgAAAAAAAAAAAAAAAAAAAAAAAAAOAA4ADgAOAA4ADgAOAA4ADgAOAA4ADgAOAA4ADgAOAA4ADgAOAA4ADgAOAA4AAAAAAAAAAAAAAAAADgAOAA4ADgAOAA4ADgAOAA4ADgAOAA4ADgAOAA4ADgAOAA4ADgAOAA4ADgAOAA4ADgAOAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA4ADgAOAA4ADgAOAA4ADgAOAA4ADgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOAA4ADgAOAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADgAOAA4ADgAOAA4ADgAOAAAAAAAAAAAAAAAAAAAAAAAAAAAADgAOAA4ADgAOAA4AAAAAAAAAAAAAAAAAAAAAAA4ADgAOAA4ADgAOAA4ADgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOAA4ADgAOAA4ADgAOAA4ADgAOAA4ADgAOAA4ADgAOAA4ADgAOAA4ADgAOAA4ADgAOAA4ADgAOAA4AAAAOAA4ADgAOAA4ADgAAAA4ADgAOAA4ADgAOAA4ADgAOAA4ADgAOAA4ADgAOAA4ADgAOAA4ADgAOAA4ADgAOAA4AAAAAAAAAAAA='; /* * utrie 1.0.2 * Copyright (c) 2022 Niklas von Hertzen * Released under MIT License */ var chars$1 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'; // Use a lookup table to find the index. var lookup$1 = typeof Uint8Array === 'undefined' ? [] : new Uint8Array(256); for (var i$1 = 0; i$1 < chars$1.length; i$1++) { lookup$1[chars$1.charCodeAt(i$1)] = i$1; } var decode$a = function (base64) { var bufferLength = base64.length * 0.75, len = base64.length, i, p = 0, encoded1, encoded2, encoded3, encoded4; if (base64[base64.length - 1] === '=') { bufferLength--; if (base64[base64.length - 2] === '=') { bufferLength--; } } var buffer = typeof ArrayBuffer !== 'undefined' && typeof Uint8Array !== 'undefined' && typeof Uint8Array.prototype.slice !== 'undefined' ? new ArrayBuffer(bufferLength) : new Array(bufferLength); var bytes = Array.isArray(buffer) ? buffer : new Uint8Array(buffer); for (i = 0; i < len; i += 4) { encoded1 = lookup$1[base64.charCodeAt(i)]; encoded2 = lookup$1[base64.charCodeAt(i + 1)]; encoded3 = lookup$1[base64.charCodeAt(i + 2)]; encoded4 = lookup$1[base64.charCodeAt(i + 3)]; bytes[p++] = (encoded1 << 2) | (encoded2 >> 4); bytes[p++] = ((encoded2 & 15) << 4) | (encoded3 >> 2); bytes[p++] = ((encoded3 & 3) << 6) | (encoded4 & 63); } return buffer; }; var polyUint16Array = function (buffer) { var length = buffer.length; var bytes = []; for (var i = 0; i < length; i += 2) { bytes.push((buffer[i + 1] << 8) | buffer[i]); } return bytes; }; var polyUint32Array = function (buffer) { var length = buffer.length; var bytes = []; for (var i = 0; i < length; i += 4) { bytes.push((buffer[i + 3] << 24) | (buffer[i + 2] << 16) | (buffer[i + 1] << 8) | buffer[i]); } return bytes; }; /** Shift size for getting the index-2 table offset. */ var UTRIE2_SHIFT_2 = 5; /** Shift size for getting the index-1 table offset. */ var UTRIE2_SHIFT_1 = 6 + 5; /** * Shift size for shifting left the index array values. * Increases possible data size with 16-bit index values at the cost * of compactability. * This requires data blocks to be aligned by UTRIE2_DATA_GRANULARITY. */ var UTRIE2_INDEX_SHIFT = 2; /** * Difference between the two shift sizes, * for getting an index-1 offset from an index-2 offset. 6=11-5 */ var UTRIE2_SHIFT_1_2 = UTRIE2_SHIFT_1 - UTRIE2_SHIFT_2; /** * The part of the index-2 table for U+D800..U+DBFF stores values for * lead surrogate code _units_ not code _points_. * Values for lead surrogate code _points_ are indexed with this portion of the table. * Length=32=0x20=0x400>>UTRIE2_SHIFT_2. (There are 1024=0x400 lead surrogates.) */ var UTRIE2_LSCP_INDEX_2_OFFSET = 0x10000 >> UTRIE2_SHIFT_2; /** Number of entries in a data block. 32=0x20 */ var UTRIE2_DATA_BLOCK_LENGTH = 1 << UTRIE2_SHIFT_2; /** Mask for getting the lower bits for the in-data-block offset. */ var UTRIE2_DATA_MASK = UTRIE2_DATA_BLOCK_LENGTH - 1; var UTRIE2_LSCP_INDEX_2_LENGTH = 0x400 >> UTRIE2_SHIFT_2; /** Count the lengths of both BMP pieces. 2080=0x820 */ var UTRIE2_INDEX_2_BMP_LENGTH = UTRIE2_LSCP_INDEX_2_OFFSET + UTRIE2_LSCP_INDEX_2_LENGTH; /** * The 2-byte UTF-8 version of the index-2 table follows at offset 2080=0x820. * Length 32=0x20 for lead bytes C0..DF, regardless of UTRIE2_SHIFT_2. */ var UTRIE2_UTF8_2B_INDEX_2_OFFSET = UTRIE2_INDEX_2_BMP_LENGTH; var UTRIE2_UTF8_2B_INDEX_2_LENGTH = 0x800 >> 6; /* U+0800 is the first code point after 2-byte UTF-8 */ /** * The index-1 table, only used for supplementary code points, at offset 2112=0x840. * Variable length, for code points up to highStart, where the last single-value range starts. * Maximum length 512=0x200=0x100000>>UTRIE2_SHIFT_1. * (For 0x100000 supplementary code points U+10000..U+10ffff.) * * The part of the index-2 table for supplementary code points starts * after this index-1 table. * * Both the index-1 table and the following part of the index-2 table * are omitted completely if there is only BMP data. */ var UTRIE2_INDEX_1_OFFSET = UTRIE2_UTF8_2B_INDEX_2_OFFSET + UTRIE2_UTF8_2B_INDEX_2_LENGTH; /** * Number of index-1 entries for the BMP. 32=0x20 * This part of the index-1 table is omitted from the serialized form. */ var UTRIE2_OMITTED_BMP_INDEX_1_LENGTH = 0x10000 >> UTRIE2_SHIFT_1; /** Number of entries in an index-2 block. 64=0x40 */ var UTRIE2_INDEX_2_BLOCK_LENGTH = 1 << UTRIE2_SHIFT_1_2; /** Mask for getting the lower bits for the in-index-2-block offset. */ var UTRIE2_INDEX_2_MASK = UTRIE2_INDEX_2_BLOCK_LENGTH - 1; var slice16 = function (view, start, end) { if (view.slice) { return view.slice(start, end); } return new Uint16Array(Array.prototype.slice.call(view, start, end)); }; var slice32 = function (view, start, end) { if (view.slice) { return view.slice(start, end); } return new Uint32Array(Array.prototype.slice.call(view, start, end)); }; var createTrieFromBase64 = function (base64, _byteLength) { var buffer = decode$a(base64); var view32 = Array.isArray(buffer) ? polyUint32Array(buffer) : new Uint32Array(buffer); var view16 = Array.isArray(buffer) ? polyUint16Array(buffer) : new Uint16Array(buffer); var headerLength = 24; var index = slice16(view16, headerLength / 2, view32[4] / 2); var data = view32[5] === 2 ? slice16(view16, (headerLength + view32[4]) / 2) : slice32(view32, Math.ceil((headerLength + view32[4]) / 4)); return new Trie(view32[0], view32[1], view32[2], view32[3], index, data); }; var Trie = /** @class */ (function () { function Trie(initialValue, errorValue, highStart, highValueIndex, index, data) { this.initialValue = initialValue; this.errorValue = errorValue; this.highStart = highStart; this.highValueIndex = highValueIndex; this.index = index; this.data = data; } /** * Get the value for a code point as stored in the Trie. * * @param codePoint the code point * @return the value */ Trie.prototype.get = function (codePoint) { var ix; if (codePoint >= 0) { if (codePoint < 0x0d800 || (codePoint > 0x0dbff && codePoint <= 0x0ffff)) { // Ordinary BMP code point, excluding leading surrogates. // BMP uses a single level lookup. BMP index starts at offset 0 in the Trie2 index. // 16 bit data is stored in the index array itself. ix = this.index[codePoint >> UTRIE2_SHIFT_2]; ix = (ix << UTRIE2_INDEX_SHIFT) + (codePoint & UTRIE2_DATA_MASK); return this.data[ix]; } if (codePoint <= 0xffff) { // Lead Surrogate Code Point. A Separate index section is stored for // lead surrogate code units and code points. // The main index has the code unit data. // For this function, we need the code point data. // Note: this expression could be refactored for slightly improved efficiency, but // surrogate code points will be so rare in practice that it's not worth it. ix = this.index[UTRIE2_LSCP_INDEX_2_OFFSET + ((codePoint - 0xd800) >> UTRIE2_SHIFT_2)]; ix = (ix << UTRIE2_INDEX_SHIFT) + (codePoint & UTRIE2_DATA_MASK); return this.data[ix]; } if (codePoint < this.highStart) { // Supplemental code point, use two-level lookup. ix = UTRIE2_INDEX_1_OFFSET - UTRIE2_OMITTED_BMP_INDEX_1_LENGTH + (codePoint >> UTRIE2_SHIFT_1); ix = this.index[ix]; ix += (codePoint >> UTRIE2_SHIFT_2) & UTRIE2_INDEX_2_MASK; ix = this.index[ix]; ix = (ix << UTRIE2_INDEX_SHIFT) + (codePoint & UTRIE2_DATA_MASK); return this.data[ix]; } if (codePoint <= 0x10ffff) { return this.data[this.highValueIndex]; } } // Fall through. The code point is outside of the legal range of 0..0x10ffff. return this.errorValue; }; return Trie; }()); /* * base64-arraybuffer 1.0.2 * Copyright (c) 2022 Niklas von Hertzen * Released under MIT License */ var chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'; // Use a lookup table to find the index. var lookup = typeof Uint8Array === 'undefined' ? [] : new Uint8Array(256); for (var i = 0; i < chars.length; i++) { lookup[chars.charCodeAt(i)] = i; } var Prepend = 1; var CR = 2; var LF = 3; var Control = 4; var Extend = 5; var SpacingMark = 7; var L = 8; var V = 9; var T = 10; var LV = 11; var LVT = 12; var ZWJ = 13; var Extended_Pictographic = 14; var RI = 15; var toCodePoints = function (str) { var codePoints = []; var i = 0; var length = str.length; while (i < length) { var value = str.charCodeAt(i++); if (value >= 0xd800 && value <= 0xdbff && i < length) { var extra = str.charCodeAt(i++); if ((extra & 0xfc00) === 0xdc00) { codePoints.push(((value & 0x3ff) << 10) + (extra & 0x3ff) + 0x10000); } else { codePoints.push(value); i--; } } else { codePoints.push(value); } } return codePoints; }; var fromCodePoint = function () { var codePoints = []; for (var _i = 0; _i < arguments.length; _i++) { codePoints[_i] = arguments[_i]; } if (String.fromCodePoint) { return String.fromCodePoint.apply(String, codePoints); } var length = codePoints.length; if (!length) { return ''; } var codeUnits = []; var index = -1; var result = ''; while (++index < length) { var codePoint = codePoints[index]; if (codePoint <= 0xffff) { codeUnits.push(codePoint); } else { codePoint -= 0x10000; codeUnits.push((codePoint >> 10) + 0xd800, (codePoint % 0x400) + 0xdc00); } if (index + 1 === length || codeUnits.length > 0x4000) { result += String.fromCharCode.apply(String, codeUnits); codeUnits.length = 0; } } return result; }; var UnicodeTrie = createTrieFromBase64(base64); var BREAK_NOT_ALLOWED = '×'; var BREAK_ALLOWED = '÷'; var codePointToClass = function (codePoint) { return UnicodeTrie.get(codePoint); }; var _graphemeBreakAtIndex = function (_codePoints, classTypes, index) { var prevIndex = index - 2; var prev = classTypes[prevIndex]; var current = classTypes[index - 1]; var next = classTypes[index]; // GB3 Do not break between a CR and LF if (current === CR && next === LF) { return BREAK_NOT_ALLOWED; } // GB4 Otherwise, break before and after controls. if (current === CR || current === LF || current === Control) { return BREAK_ALLOWED; } // GB5 if (next === CR || next === LF || next === Control) { return BREAK_ALLOWED; } // Do not break Hangul syllable sequences. // GB6 if (current === L && [L, V, LV, LVT].indexOf(next) !== -1) { return BREAK_NOT_ALLOWED; } // GB7 if ((current === LV || current === V) && (next === V || next === T)) { return BREAK_NOT_ALLOWED; } // GB8 if ((current === LVT || current === T) && next === T) { return BREAK_NOT_ALLOWED; } // GB9 Do not break before extending characters or ZWJ. if (next === ZWJ || next === Extend) { return BREAK_NOT_ALLOWED; } // Do not break before SpacingMarks, or after Prepend characters. // GB9a if (next === SpacingMark) { return BREAK_NOT_ALLOWED; } // GB9a if (current === Prepend) { return BREAK_NOT_ALLOWED; } // GB11 Do not break within emoji modifier sequences or emoji zwj sequences. if (current === ZWJ && next === Extended_Pictographic) { while (prev === Extend) { prev = classTypes[--prevIndex]; } if (prev === Extended_Pictographic) { return BREAK_NOT_ALLOWED; } } // GB12 Do not break within emoji flag sequences. // That is, do not break between regional indicator (RI) symbols // if there is an odd number of RI characters before the break point. if (current === RI && next === RI) { var countRI = 0; while (prev === RI) { countRI++; prev = classTypes[--prevIndex]; } if (countRI % 2 === 0) { return BREAK_NOT_ALLOWED; } } return BREAK_ALLOWED; }; var GraphemeBreaker = function (str) { var codePoints = toCodePoints(str); var length = codePoints.length; var index = 0; var lastEnd = 0; var classTypes = codePoints.map(codePointToClass); return { next: function () { if (index >= length) { return { done: true, value: null }; } var graphemeBreak = BREAK_NOT_ALLOWED; while (index < length && (graphemeBreak = _graphemeBreakAtIndex(codePoints, classTypes, ++index)) === BREAK_NOT_ALLOWED) { } if (graphemeBreak !== BREAK_NOT_ALLOWED || index === length) { var value = fromCodePoint.apply(null, codePoints.slice(lastEnd, index)); lastEnd = index; return { value: value, done: false }; } return { done: true, value: null }; }, }; }; var splitGraphemes = function (str) { var breaker = GraphemeBreaker(str); var graphemes = []; var bk; while (!(bk = breaker.next()).done) { if (bk.value) { graphemes.push(bk.value.slice()); } } return graphemes; }; var testRangeBounds = function (document) { var TEST_HEIGHT = 123; if (document.createRange) { var range = document.createRange(); if (range.getBoundingClientRect) { var testElement = document.createElement('boundtest'); testElement.style.height = TEST_HEIGHT + "px"; testElement.style.display = 'block'; document.body.appendChild(testElement); range.selectNode(testElement); var rangeBounds = range.getBoundingClientRect(); var rangeHeight = Math.round(rangeBounds.height); document.body.removeChild(testElement); if (rangeHeight === TEST_HEIGHT) { return true; } } } return false; }; var testIOSLineBreak = function (document) { var testElement = document.createElement('boundtest'); testElement.style.width = '50px'; testElement.style.display = 'block'; testElement.style.fontSize = '12px'; testElement.style.letterSpacing = '0px'; testElement.style.wordSpacing = '0px'; document.body.appendChild(testElement); var range = document.createRange(); testElement.innerHTML = typeof ''.repeat === 'function' ? '👨'.repeat(10) : ''; var node = testElement.firstChild; var textList = toCodePoints$1(node.data).map(function (i) { return fromCodePoint$1(i); }); var offset = 0; var prev = {}; // ios 13 does not handle range getBoundingClientRect line changes correctly #2177 var supports = textList.every(function (text, i) { range.setStart(node, offset); range.setEnd(node, offset + text.length); var rect = range.getBoundingClientRect(); offset += text.length; var boundAhead = rect.x > prev.x || rect.y > prev.y; prev = rect; if (i === 0) { return true; } return boundAhead; }); document.body.removeChild(testElement); return supports; }; var testCORS = function () { return typeof new Image().crossOrigin !== 'undefined'; }; var testResponseType = function () { return typeof new XMLHttpRequest().responseType === 'string'; }; var testSVG = function (document) { var img = new Image(); var canvas = document.createElement('canvas'); var ctx = canvas.getContext('2d'); if (!ctx) { return false; } img.src = "data:image/svg+xml,"; try { ctx.drawImage(img, 0, 0); canvas.toDataURL(); } catch (e) { return false; } return true; }; var isGreenPixel = function (data) { return data[0] === 0 && data[1] === 255 && data[2] === 0 && data[3] === 255; }; var testForeignObject = function (document) { var canvas = document.createElement('canvas'); var size = 100; canvas.width = size; canvas.height = size; var ctx = canvas.getContext('2d'); if (!ctx) { return Promise.reject(false); } ctx.fillStyle = 'rgb(0, 255, 0)'; ctx.fillRect(0, 0, size, size); var img = new Image(); var greenImageSrc = canvas.toDataURL(); img.src = greenImageSrc; var svg = createForeignObjectSVG(size, size, 0, 0, img); ctx.fillStyle = 'red'; ctx.fillRect(0, 0, size, size); return loadSerializedSVG$1(svg) .then(function (img) { ctx.drawImage(img, 0, 0); var data = ctx.getImageData(0, 0, size, size).data; ctx.fillStyle = 'red'; ctx.fillRect(0, 0, size, size); var node = document.createElement('div'); node.style.backgroundImage = "url(" + greenImageSrc + ")"; node.style.height = size + "px"; // Firefox 55 does not render inline tags return isGreenPixel(data) ? loadSerializedSVG$1(createForeignObjectSVG(size, size, 0, 0, node)) : Promise.reject(false); }) .then(function (img) { ctx.drawImage(img, 0, 0); // Edge does not render background-images return isGreenPixel(ctx.getImageData(0, 0, size, size).data); }) .catch(function () { return false; }); }; var createForeignObjectSVG = function (width, height, x, y, node) { var xmlns = 'http://www.w3.org/2000/svg'; var svg = document.createElementNS(xmlns, 'svg'); var foreignObject = document.createElementNS(xmlns, 'foreignObject'); svg.setAttributeNS(null, 'width', width.toString()); svg.setAttributeNS(null, 'height', height.toString()); foreignObject.setAttributeNS(null, 'width', '100%'); foreignObject.setAttributeNS(null, 'height', '100%'); foreignObject.setAttributeNS(null, 'x', x.toString()); foreignObject.setAttributeNS(null, 'y', y.toString()); foreignObject.setAttributeNS(null, 'externalResourcesRequired', 'true'); svg.appendChild(foreignObject); foreignObject.appendChild(node); return svg; }; var loadSerializedSVG$1 = function (svg) { return new Promise(function (resolve, reject) { var img = new Image(); img.onload = function () { return resolve(img); }; img.onerror = reject; img.src = "data:image/svg+xml;charset=utf-8," + encodeURIComponent(new XMLSerializer().serializeToString(svg)); }); }; var FEATURES = { get SUPPORT_RANGE_BOUNDS() { var value = testRangeBounds(document); Object.defineProperty(FEATURES, 'SUPPORT_RANGE_BOUNDS', { value: value }); return value; }, get SUPPORT_WORD_BREAKING() { var value = FEATURES.SUPPORT_RANGE_BOUNDS && testIOSLineBreak(document); Object.defineProperty(FEATURES, 'SUPPORT_WORD_BREAKING', { value: value }); return value; }, get SUPPORT_SVG_DRAWING() { var value = testSVG(document); Object.defineProperty(FEATURES, 'SUPPORT_SVG_DRAWING', { value: value }); return value; }, get SUPPORT_FOREIGNOBJECT_DRAWING() { var value = typeof Array.from === 'function' && typeof window.fetch === 'function' ? testForeignObject(document) : Promise.resolve(false); Object.defineProperty(FEATURES, 'SUPPORT_FOREIGNOBJECT_DRAWING', { value: value }); return value; }, get SUPPORT_CORS_IMAGES() { var value = testCORS(); Object.defineProperty(FEATURES, 'SUPPORT_CORS_IMAGES', { value: value }); return value; }, get SUPPORT_RESPONSE_TYPE() { var value = testResponseType(); Object.defineProperty(FEATURES, 'SUPPORT_RESPONSE_TYPE', { value: value }); return value; }, get SUPPORT_CORS_XHR() { var value = 'withCredentials' in new XMLHttpRequest(); Object.defineProperty(FEATURES, 'SUPPORT_CORS_XHR', { value: value }); return value; }, get SUPPORT_NATIVE_TEXT_SEGMENTATION() { // eslint-disable-next-line @typescript-eslint/no-explicit-any var value = !!(typeof Intl !== 'undefined' && Intl.Segmenter); Object.defineProperty(FEATURES, 'SUPPORT_NATIVE_TEXT_SEGMENTATION', { value: value }); return value; } }; var TextBounds = /** @class */ (function () { function TextBounds(text, bounds) { this.text = text; this.bounds = bounds; } return TextBounds; }()); var parseTextBounds = function (context, value, styles, node) { var textList = breakText(value, styles); var textBounds = []; var offset = 0; textList.forEach(function (text) { if (styles.textDecorationLine.length || text.trim().length > 0) { if (FEATURES.SUPPORT_RANGE_BOUNDS) { var clientRects = createRange(node, offset, text.length).getClientRects(); if (clientRects.length > 1) { var subSegments = segmentGraphemes(text); var subOffset_1 = 0; subSegments.forEach(function (subSegment) { textBounds.push(new TextBounds(subSegment, Bounds.fromDOMRectList(context, createRange(node, subOffset_1 + offset, subSegment.length).getClientRects()))); subOffset_1 += subSegment.length; }); } else { textBounds.push(new TextBounds(text, Bounds.fromDOMRectList(context, clientRects))); } } else { var replacementNode = node.splitText(text.length); textBounds.push(new TextBounds(text, getWrapperBounds(context, node))); node = replacementNode; } } else if (!FEATURES.SUPPORT_RANGE_BOUNDS) { node = node.splitText(text.length); } offset += text.length; }); return textBounds; }; var getWrapperBounds = function (context, node) { var ownerDocument = node.ownerDocument; if (ownerDocument) { var wrapper = ownerDocument.createElement('html2canvaswrapper'); wrapper.appendChild(node.cloneNode(true)); var parentNode = node.parentNode; if (parentNode) { parentNode.replaceChild(wrapper, node); var bounds = parseBounds(context, wrapper); if (wrapper.firstChild) { parentNode.replaceChild(wrapper.firstChild, wrapper); } return bounds; } } return Bounds.EMPTY; }; var createRange = function (node, offset, length) { var ownerDocument = node.ownerDocument; if (!ownerDocument) { throw new Error('Node has no owner document'); } var range = ownerDocument.createRange(); range.setStart(node, offset); range.setEnd(node, offset + length); return range; }; var segmentGraphemes = function (value) { if (FEATURES.SUPPORT_NATIVE_TEXT_SEGMENTATION) { // eslint-disable-next-line @typescript-eslint/no-explicit-any var segmenter = new Intl.Segmenter(void 0, { granularity: 'grapheme' }); // eslint-disable-next-line @typescript-eslint/no-explicit-any return Array.from(segmenter.segment(value)).map(function (segment) { return segment.segment; }); } return splitGraphemes(value); }; var segmentWords = function (value, styles) { if (FEATURES.SUPPORT_NATIVE_TEXT_SEGMENTATION) { // eslint-disable-next-line @typescript-eslint/no-explicit-any var segmenter = new Intl.Segmenter(void 0, { granularity: 'word' }); // eslint-disable-next-line @typescript-eslint/no-explicit-any return Array.from(segmenter.segment(value)).map(function (segment) { return segment.segment; }); } return breakWords(value, styles); }; var breakText = function (value, styles) { return styles.letterSpacing !== 0 ? segmentGraphemes(value) : segmentWords(value, styles); }; // https://drafts.csswg.org/css-text/#word-separator var wordSeparators = [0x0020, 0x00a0, 0x1361, 0x10100, 0x10101, 0x1039, 0x1091]; var breakWords = function (str, styles) { var breaker = LineBreaker(str, { lineBreak: styles.lineBreak, wordBreak: styles.overflowWrap === "break-word" /* BREAK_WORD */ ? 'break-word' : styles.wordBreak }); var words = []; var bk; var _loop_1 = function () { if (bk.value) { var value = bk.value.slice(); var codePoints = toCodePoints$1(value); var word_1 = ''; codePoints.forEach(function (codePoint) { if (wordSeparators.indexOf(codePoint) === -1) { word_1 += fromCodePoint$1(codePoint); } else { if (word_1.length) { words.push(word_1); } words.push(fromCodePoint$1(codePoint)); word_1 = ''; } }); if (word_1.length) { words.push(word_1); } } }; while (!(bk = breaker.next()).done) { _loop_1(); } return words; }; var TextContainer = /** @class */ (function () { function TextContainer(context, node, styles) { this.text = transform(node.data, styles.textTransform); this.textBounds = parseTextBounds(context, this.text, styles, node); } return TextContainer; }()); var transform = function (text, transform) { switch (transform) { case 1 /* LOWERCASE */: return text.toLowerCase(); case 3 /* CAPITALIZE */: return text.replace(CAPITALIZE, capitalize); case 2 /* UPPERCASE */: return text.toUpperCase(); default: return text; } }; var CAPITALIZE = /(^|\s|:|-|\(|\))([a-z])/g; var capitalize = function (m, p1, p2) { if (m.length > 0) { return p1 + p2.toUpperCase(); } return m; }; var ImageElementContainer = /** @class */ (function (_super) { __extends(ImageElementContainer, _super); function ImageElementContainer(context, img) { var _this = _super.call(this, context, img) || this; _this.src = img.currentSrc || img.src; _this.intrinsicWidth = img.naturalWidth; _this.intrinsicHeight = img.naturalHeight; _this.context.cache.addImage(_this.src); return _this; } return ImageElementContainer; }(ElementContainer)); var CanvasElementContainer = /** @class */ (function (_super) { __extends(CanvasElementContainer, _super); function CanvasElementContainer(context, canvas) { var _this = _super.call(this, context, canvas) || this; _this.canvas = canvas; _this.intrinsicWidth = canvas.width; _this.intrinsicHeight = canvas.height; return _this; } return CanvasElementContainer; }(ElementContainer)); var SVGElementContainer = /** @class */ (function (_super) { __extends(SVGElementContainer, _super); function SVGElementContainer(context, img) { var _this = _super.call(this, context, img) || this; var s = new XMLSerializer(); var bounds = parseBounds(context, img); img.setAttribute('width', bounds.width + "px"); img.setAttribute('height', bounds.height + "px"); _this.svg = "data:image/svg+xml," + encodeURIComponent(s.serializeToString(img)); _this.intrinsicWidth = img.width.baseVal.value; _this.intrinsicHeight = img.height.baseVal.value; _this.context.cache.addImage(_this.svg); return _this; } return SVGElementContainer; }(ElementContainer)); var LIElementContainer = /** @class */ (function (_super) { __extends(LIElementContainer, _super); function LIElementContainer(context, element) { var _this = _super.call(this, context, element) || this; _this.value = element.value; return _this; } return LIElementContainer; }(ElementContainer)); var OLElementContainer = /** @class */ (function (_super) { __extends(OLElementContainer, _super); function OLElementContainer(context, element) { var _this = _super.call(this, context, element) || this; _this.start = element.start; _this.reversed = typeof element.reversed === 'boolean' && element.reversed === true; return _this; } return OLElementContainer; }(ElementContainer)); var CHECKBOX_BORDER_RADIUS = [ { type: 15 /* DIMENSION_TOKEN */, flags: 0, unit: 'px', number: 3 } ]; var RADIO_BORDER_RADIUS = [ { type: 16 /* PERCENTAGE_TOKEN */, flags: 0, number: 50 } ]; var reformatInputBounds = function (bounds) { if (bounds.width > bounds.height) { return new Bounds(bounds.left + (bounds.width - bounds.height) / 2, bounds.top, bounds.height, bounds.height); } else if (bounds.width < bounds.height) { return new Bounds(bounds.left, bounds.top + (bounds.height - bounds.width) / 2, bounds.width, bounds.width); } return bounds; }; var getInputValue = function (node) { var value = node.type === PASSWORD ? new Array(node.value.length + 1).join('\u2022') : node.value; return value.length === 0 ? node.placeholder || '' : value; }; var CHECKBOX = 'checkbox'; var RADIO = 'radio'; var PASSWORD = 'password'; var INPUT_COLOR = 0x2a2a2aff; var InputElementContainer = /** @class */ (function (_super) { __extends(InputElementContainer, _super); function InputElementContainer(context, input) { var _this = _super.call(this, context, input) || this; _this.type = input.type.toLowerCase(); _this.checked = input.checked; _this.value = getInputValue(input); if (_this.type === CHECKBOX || _this.type === RADIO) { _this.styles.backgroundColor = 0xdededeff; _this.styles.borderTopColor = _this.styles.borderRightColor = _this.styles.borderBottomColor = _this.styles.borderLeftColor = 0xa5a5a5ff; _this.styles.borderTopWidth = _this.styles.borderRightWidth = _this.styles.borderBottomWidth = _this.styles.borderLeftWidth = 1; _this.styles.borderTopStyle = _this.styles.borderRightStyle = _this.styles.borderBottomStyle = _this.styles.borderLeftStyle = 1 /* SOLID */; _this.styles.backgroundClip = [0 /* BORDER_BOX */]; _this.styles.backgroundOrigin = [0 /* BORDER_BOX */]; _this.bounds = reformatInputBounds(_this.bounds); } switch (_this.type) { case CHECKBOX: _this.styles.borderTopRightRadius = _this.styles.borderTopLeftRadius = _this.styles.borderBottomRightRadius = _this.styles.borderBottomLeftRadius = CHECKBOX_BORDER_RADIUS; break; case RADIO: _this.styles.borderTopRightRadius = _this.styles.borderTopLeftRadius = _this.styles.borderBottomRightRadius = _this.styles.borderBottomLeftRadius = RADIO_BORDER_RADIUS; break; } return _this; } return InputElementContainer; }(ElementContainer)); var SelectElementContainer = /** @class */ (function (_super) { __extends(SelectElementContainer, _super); function SelectElementContainer(context, element) { var _this = _super.call(this, context, element) || this; var option = element.options[element.selectedIndex || 0]; _this.value = option ? option.text || '' : ''; return _this; } return SelectElementContainer; }(ElementContainer)); var TextareaElementContainer = /** @class */ (function (_super) { __extends(TextareaElementContainer, _super); function TextareaElementContainer(context, element) { var _this = _super.call(this, context, element) || this; _this.value = element.value; return _this; } return TextareaElementContainer; }(ElementContainer)); var IFrameElementContainer = /** @class */ (function (_super) { __extends(IFrameElementContainer, _super); function IFrameElementContainer(context, iframe) { var _this = _super.call(this, context, iframe) || this; _this.src = iframe.src; _this.width = parseInt(iframe.width, 10) || 0; _this.height = parseInt(iframe.height, 10) || 0; _this.backgroundColor = _this.styles.backgroundColor; try { if (iframe.contentWindow && iframe.contentWindow.document && iframe.contentWindow.document.documentElement) { _this.tree = parseTree(context, iframe.contentWindow.document.documentElement); // http://www.w3.org/TR/css3-background/#special-backgrounds var documentBackgroundColor = iframe.contentWindow.document.documentElement ? parseColor(context, getComputedStyle(iframe.contentWindow.document.documentElement).backgroundColor) : COLORS.TRANSPARENT; var bodyBackgroundColor = iframe.contentWindow.document.body ? parseColor(context, getComputedStyle(iframe.contentWindow.document.body).backgroundColor) : COLORS.TRANSPARENT; _this.backgroundColor = isTransparent(documentBackgroundColor) ? isTransparent(bodyBackgroundColor) ? _this.styles.backgroundColor : bodyBackgroundColor : documentBackgroundColor; } } catch (e) { } return _this; } return IFrameElementContainer; }(ElementContainer)); var LIST_OWNERS = ['OL', 'UL', 'MENU']; var parseNodeTree = function (context, node, parent, root) { for (var childNode = node.firstChild, nextNode = void 0; childNode; childNode = nextNode) { nextNode = childNode.nextSibling; if (isTextNode(childNode) && childNode.data.trim().length > 0) { parent.textNodes.push(new TextContainer(context, childNode, parent.styles)); } else if (isElementNode(childNode)) { if (isSlotElement(childNode) && childNode.assignedNodes) { childNode.assignedNodes().forEach(function (childNode) { return parseNodeTree(context, childNode, parent, root); }); } else { var container = createContainer(context, childNode); if (container.styles.isVisible()) { if (createsRealStackingContext(childNode, container, root)) { container.flags |= 4 /* CREATES_REAL_STACKING_CONTEXT */; } else if (createsStackingContext(container.styles)) { container.flags |= 2 /* CREATES_STACKING_CONTEXT */; } if (LIST_OWNERS.indexOf(childNode.tagName) !== -1) { container.flags |= 8 /* IS_LIST_OWNER */; } parent.elements.push(container); childNode.slot; if (childNode.shadowRoot) { parseNodeTree(context, childNode.shadowRoot, container, root); } else if (!isTextareaElement(childNode) && !isSVGElement(childNode) && !isSelectElement(childNode)) { parseNodeTree(context, childNode, container, root); } } } } } }; var createContainer = function (context, element) { if (isImageElement(element)) { return new ImageElementContainer(context, element); } if (isCanvasElement(element)) { return new CanvasElementContainer(context, element); } if (isSVGElement(element)) { return new SVGElementContainer(context, element); } if (isLIElement(element)) { return new LIElementContainer(context, element); } if (isOLElement(element)) { return new OLElementContainer(context, element); } if (isInputElement(element)) { return new InputElementContainer(context, element); } if (isSelectElement(element)) { return new SelectElementContainer(context, element); } if (isTextareaElement(element)) { return new TextareaElementContainer(context, element); } if (isIFrameElement(element)) { return new IFrameElementContainer(context, element); } return new ElementContainer(context, element); }; var parseTree = function (context, element) { var container = createContainer(context, element); container.flags |= 4 /* CREATES_REAL_STACKING_CONTEXT */; parseNodeTree(context, element, container, container); return container; }; var createsRealStackingContext = function (node, container, root) { return (container.styles.isPositionedWithZIndex() || container.styles.opacity < 1 || container.styles.isTransformed() || (isBodyElement(node) && root.styles.isTransparent())); }; var createsStackingContext = function (styles) { return styles.isPositioned() || styles.isFloating(); }; var isTextNode = function (node) { return node.nodeType === Node.TEXT_NODE; }; var isElementNode = function (node) { return node.nodeType === Node.ELEMENT_NODE; }; var isHTMLElementNode = function (node) { return isElementNode(node) && typeof node.style !== 'undefined' && !isSVGElementNode(node); }; var isSVGElementNode = function (element) { return typeof element.className === 'object'; }; var isLIElement = function (node) { return node.tagName === 'LI'; }; var isOLElement = function (node) { return node.tagName === 'OL'; }; var isInputElement = function (node) { return node.tagName === 'INPUT'; }; var isHTMLElement = function (node) { return node.tagName === 'HTML'; }; var isSVGElement = function (node) { return node.tagName === 'svg'; }; var isBodyElement = function (node) { return node.tagName === 'BODY'; }; var isCanvasElement = function (node) { return node.tagName === 'CANVAS'; }; var isVideoElement = function (node) { return node.tagName === 'VIDEO'; }; var isImageElement = function (node) { return node.tagName === 'IMG'; }; var isIFrameElement = function (node) { return node.tagName === 'IFRAME'; }; var isStyleElement = function (node) { return node.tagName === 'STYLE'; }; var isScriptElement = function (node) { return node.tagName === 'SCRIPT'; }; var isTextareaElement = function (node) { return node.tagName === 'TEXTAREA'; }; var isSelectElement = function (node) { return node.tagName === 'SELECT'; }; var isSlotElement = function (node) { return node.tagName === 'SLOT'; }; // https://html.spec.whatwg.org/multipage/custom-elements.html#valid-custom-element-name var isCustomElement = function (node) { return node.tagName.indexOf('-') > 0; }; var CounterState = /** @class */ (function () { function CounterState() { this.counters = {}; } CounterState.prototype.getCounterValue = function (name) { var counter = this.counters[name]; if (counter && counter.length) { return counter[counter.length - 1]; } return 1; }; CounterState.prototype.getCounterValues = function (name) { var counter = this.counters[name]; return counter ? counter : []; }; CounterState.prototype.pop = function (counters) { var _this = this; counters.forEach(function (counter) { return _this.counters[counter].pop(); }); }; CounterState.prototype.parse = function (style) { var _this = this; var counterIncrement = style.counterIncrement; var counterReset = style.counterReset; var canReset = true; if (counterIncrement !== null) { counterIncrement.forEach(function (entry) { var counter = _this.counters[entry.counter]; if (counter && entry.increment !== 0) { canReset = false; if (!counter.length) { counter.push(1); } counter[Math.max(0, counter.length - 1)] += entry.increment; } }); } var counterNames = []; if (canReset) { counterReset.forEach(function (entry) { var counter = _this.counters[entry.counter]; counterNames.push(entry.counter); if (!counter) { counter = _this.counters[entry.counter] = []; } counter.push(entry.reset); }); } return counterNames; }; return CounterState; }()); var ROMAN_UPPER = { integers: [1000, 900, 500, 400, 100, 90, 50, 40, 10, 9, 5, 4, 1], values: ['M', 'CM', 'D', 'CD', 'C', 'XC', 'L', 'XL', 'X', 'IX', 'V', 'IV', 'I'] }; var ARMENIAN = { integers: [ 9000, 8000, 7000, 6000, 5000, 4000, 3000, 2000, 1000, 900, 800, 700, 600, 500, 400, 300, 200, 100, 90, 80, 70, 60, 50, 40, 30, 20, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1 ], values: [ 'Ք', 'Փ', 'Ւ', 'Ց', 'Ր', 'Տ', 'Վ', 'Ս', 'Ռ', 'Ջ', 'Պ', 'Չ', 'Ո', 'Շ', 'Ն', 'Յ', 'Մ', 'Ճ', 'Ղ', 'Ձ', 'Հ', 'Կ', 'Ծ', 'Խ', 'Լ', 'Ի', 'Ժ', 'Թ', 'Ը', 'Է', 'Զ', 'Ե', 'Դ', 'Գ', 'Բ', 'Ա' ] }; var HEBREW = { integers: [ 10000, 9000, 8000, 7000, 6000, 5000, 4000, 3000, 2000, 1000, 400, 300, 200, 100, 90, 80, 70, 60, 50, 40, 30, 20, 19, 18, 17, 16, 15, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1 ], values: [ 'י׳', 'ט׳', 'ח׳', 'ז׳', 'ו׳', 'ה׳', 'ד׳', 'ג׳', 'ב׳', 'א׳', 'ת', 'ש', 'ר', 'ק', 'צ', 'פ', 'ע', 'ס', 'נ', 'מ', 'ל', 'כ', 'יט', 'יח', 'יז', 'טז', 'טו', 'י', 'ט', 'ח', 'ז', 'ו', 'ה', 'ד', 'ג', 'ב', 'א' ] }; var GEORGIAN = { integers: [ 10000, 9000, 8000, 7000, 6000, 5000, 4000, 3000, 2000, 1000, 900, 800, 700, 600, 500, 400, 300, 200, 100, 90, 80, 70, 60, 50, 40, 30, 20, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1 ], values: [ 'ჵ', 'ჰ', 'ჯ', 'ჴ', 'ხ', 'ჭ', 'წ', 'ძ', 'ც', 'ჩ', 'შ', 'ყ', 'ღ', 'ქ', 'ფ', 'ჳ', 'ტ', 'ს', 'რ', 'ჟ', 'პ', 'ო', 'ჲ', 'ნ', 'მ', 'ლ', 'კ', 'ი', 'თ', 'ჱ', 'ზ', 'ვ', 'ე', 'დ', 'გ', 'ბ', 'ა' ] }; var createAdditiveCounter = function (value, min, max, symbols, fallback, suffix) { if (value < min || value > max) { return createCounterText(value, fallback, suffix.length > 0); } return (symbols.integers.reduce(function (string, integer, index) { while (value >= integer) { value -= integer; string += symbols.values[index]; } return string; }, '') + suffix); }; var createCounterStyleWithSymbolResolver = function (value, codePointRangeLength, isNumeric, resolver) { var string = ''; do { if (!isNumeric) { value--; } string = resolver(value) + string; value /= codePointRangeLength; } while (value * codePointRangeLength >= codePointRangeLength); return string; }; var createCounterStyleFromRange = function (value, codePointRangeStart, codePointRangeEnd, isNumeric, suffix) { var codePointRangeLength = codePointRangeEnd - codePointRangeStart + 1; return ((value < 0 ? '-' : '') + (createCounterStyleWithSymbolResolver(Math.abs(value), codePointRangeLength, isNumeric, function (codePoint) { return fromCodePoint$1(Math.floor(codePoint % codePointRangeLength) + codePointRangeStart); }) + suffix)); }; var createCounterStyleFromSymbols = function (value, symbols, suffix) { if (suffix === void 0) { suffix = '. '; } var codePointRangeLength = symbols.length; return (createCounterStyleWithSymbolResolver(Math.abs(value), codePointRangeLength, false, function (codePoint) { return symbols[Math.floor(codePoint % codePointRangeLength)]; }) + suffix); }; var CJK_ZEROS = 1 << 0; var CJK_TEN_COEFFICIENTS = 1 << 1; var CJK_TEN_HIGH_COEFFICIENTS = 1 << 2; var CJK_HUNDRED_COEFFICIENTS = 1 << 3; var createCJKCounter = function (value, numbers, multipliers, negativeSign, suffix, flags) { if (value < -9999 || value > 9999) { return createCounterText(value, 4 /* CJK_DECIMAL */, suffix.length > 0); } var tmp = Math.abs(value); var string = suffix; if (tmp === 0) { return numbers[0] + string; } for (var digit = 0; tmp > 0 && digit <= 4; digit++) { var coefficient = tmp % 10; if (coefficient === 0 && contains(flags, CJK_ZEROS) && string !== '') { string = numbers[coefficient] + string; } else if (coefficient > 1 || (coefficient === 1 && digit === 0) || (coefficient === 1 && digit === 1 && contains(flags, CJK_TEN_COEFFICIENTS)) || (coefficient === 1 && digit === 1 && contains(flags, CJK_TEN_HIGH_COEFFICIENTS) && value > 100) || (coefficient === 1 && digit > 1 && contains(flags, CJK_HUNDRED_COEFFICIENTS))) { string = numbers[coefficient] + (digit > 0 ? multipliers[digit - 1] : '') + string; } else if (coefficient === 1 && digit > 0) { string = multipliers[digit - 1] + string; } tmp = Math.floor(tmp / 10); } return (value < 0 ? negativeSign : '') + string; }; var CHINESE_INFORMAL_MULTIPLIERS = '十百千萬'; var CHINESE_FORMAL_MULTIPLIERS = '拾佰仟萬'; var JAPANESE_NEGATIVE = 'マイナス'; var KOREAN_NEGATIVE = '마이너스'; var createCounterText = function (value, type, appendSuffix) { var defaultSuffix = appendSuffix ? '. ' : ''; var cjkSuffix = appendSuffix ? '、' : ''; var koreanSuffix = appendSuffix ? ', ' : ''; var spaceSuffix = appendSuffix ? ' ' : ''; switch (type) { case 0 /* DISC */: return '•' + spaceSuffix; case 1 /* CIRCLE */: return '◦' + spaceSuffix; case 2 /* SQUARE */: return '◾' + spaceSuffix; case 5 /* DECIMAL_LEADING_ZERO */: var string = createCounterStyleFromRange(value, 48, 57, true, defaultSuffix); return string.length < 4 ? "0" + string : string; case 4 /* CJK_DECIMAL */: return createCounterStyleFromSymbols(value, '〇一二三四五六七八九', cjkSuffix); case 6 /* LOWER_ROMAN */: return createAdditiveCounter(value, 1, 3999, ROMAN_UPPER, 3 /* DECIMAL */, defaultSuffix).toLowerCase(); case 7 /* UPPER_ROMAN */: return createAdditiveCounter(value, 1, 3999, ROMAN_UPPER, 3 /* DECIMAL */, defaultSuffix); case 8 /* LOWER_GREEK */: return createCounterStyleFromRange(value, 945, 969, false, defaultSuffix); case 9 /* LOWER_ALPHA */: return createCounterStyleFromRange(value, 97, 122, false, defaultSuffix); case 10 /* UPPER_ALPHA */: return createCounterStyleFromRange(value, 65, 90, false, defaultSuffix); case 11 /* ARABIC_INDIC */: return createCounterStyleFromRange(value, 1632, 1641, true, defaultSuffix); case 12 /* ARMENIAN */: case 49 /* UPPER_ARMENIAN */: return createAdditiveCounter(value, 1, 9999, ARMENIAN, 3 /* DECIMAL */, defaultSuffix); case 35 /* LOWER_ARMENIAN */: return createAdditiveCounter(value, 1, 9999, ARMENIAN, 3 /* DECIMAL */, defaultSuffix).toLowerCase(); case 13 /* BENGALI */: return createCounterStyleFromRange(value, 2534, 2543, true, defaultSuffix); case 14 /* CAMBODIAN */: case 30 /* KHMER */: return createCounterStyleFromRange(value, 6112, 6121, true, defaultSuffix); case 15 /* CJK_EARTHLY_BRANCH */: return createCounterStyleFromSymbols(value, '子丑寅卯辰巳午未申酉戌亥', cjkSuffix); case 16 /* CJK_HEAVENLY_STEM */: return createCounterStyleFromSymbols(value, '甲乙丙丁戊己庚辛壬癸', cjkSuffix); case 17 /* CJK_IDEOGRAPHIC */: case 48 /* TRAD_CHINESE_INFORMAL */: return createCJKCounter(value, '零一二三四五六七八九', CHINESE_INFORMAL_MULTIPLIERS, '負', cjkSuffix, CJK_TEN_COEFFICIENTS | CJK_TEN_HIGH_COEFFICIENTS | CJK_HUNDRED_COEFFICIENTS); case 47 /* TRAD_CHINESE_FORMAL */: return createCJKCounter(value, '零壹貳參肆伍陸柒捌玖', CHINESE_FORMAL_MULTIPLIERS, '負', cjkSuffix, CJK_ZEROS | CJK_TEN_COEFFICIENTS | CJK_TEN_HIGH_COEFFICIENTS | CJK_HUNDRED_COEFFICIENTS); case 42 /* SIMP_CHINESE_INFORMAL */: return createCJKCounter(value, '零一二三四五六七八九', CHINESE_INFORMAL_MULTIPLIERS, '负', cjkSuffix, CJK_TEN_COEFFICIENTS | CJK_TEN_HIGH_COEFFICIENTS | CJK_HUNDRED_COEFFICIENTS); case 41 /* SIMP_CHINESE_FORMAL */: return createCJKCounter(value, '零壹贰叁肆伍陆柒捌玖', CHINESE_FORMAL_MULTIPLIERS, '负', cjkSuffix, CJK_ZEROS | CJK_TEN_COEFFICIENTS | CJK_TEN_HIGH_COEFFICIENTS | CJK_HUNDRED_COEFFICIENTS); case 26 /* JAPANESE_INFORMAL */: return createCJKCounter(value, '〇一二三四五六七八九', '十百千万', JAPANESE_NEGATIVE, cjkSuffix, 0); case 25 /* JAPANESE_FORMAL */: return createCJKCounter(value, '零壱弐参四伍六七八九', '拾百千万', JAPANESE_NEGATIVE, cjkSuffix, CJK_ZEROS | CJK_TEN_COEFFICIENTS | CJK_TEN_HIGH_COEFFICIENTS); case 31 /* KOREAN_HANGUL_FORMAL */: return createCJKCounter(value, '영일이삼사오육칠팔구', '십백천만', KOREAN_NEGATIVE, koreanSuffix, CJK_ZEROS | CJK_TEN_COEFFICIENTS | CJK_TEN_HIGH_COEFFICIENTS); case 33 /* KOREAN_HANJA_INFORMAL */: return createCJKCounter(value, '零一二三四五六七八九', '十百千萬', KOREAN_NEGATIVE, koreanSuffix, 0); case 32 /* KOREAN_HANJA_FORMAL */: return createCJKCounter(value, '零壹貳參四五六七八九', '拾百千', KOREAN_NEGATIVE, koreanSuffix, CJK_ZEROS | CJK_TEN_COEFFICIENTS | CJK_TEN_HIGH_COEFFICIENTS); case 18 /* DEVANAGARI */: return createCounterStyleFromRange(value, 0x966, 0x96f, true, defaultSuffix); case 20 /* GEORGIAN */: return createAdditiveCounter(value, 1, 19999, GEORGIAN, 3 /* DECIMAL */, defaultSuffix); case 21 /* GUJARATI */: return createCounterStyleFromRange(value, 0xae6, 0xaef, true, defaultSuffix); case 22 /* GURMUKHI */: return createCounterStyleFromRange(value, 0xa66, 0xa6f, true, defaultSuffix); case 22 /* HEBREW */: return createAdditiveCounter(value, 1, 10999, HEBREW, 3 /* DECIMAL */, defaultSuffix); case 23 /* HIRAGANA */: return createCounterStyleFromSymbols(value, 'あいうえおかきくけこさしすせそたちつてとなにぬねのはひふへほまみむめもやゆよらりるれろわゐゑをん'); case 24 /* HIRAGANA_IROHA */: return createCounterStyleFromSymbols(value, 'いろはにほへとちりぬるをわかよたれそつねならむうゐのおくやまけふこえてあさきゆめみしゑひもせす'); case 27 /* KANNADA */: return createCounterStyleFromRange(value, 0xce6, 0xcef, true, defaultSuffix); case 28 /* KATAKANA */: return createCounterStyleFromSymbols(value, 'アイウエオカキクケコサシスセソタチツテトナニヌネノハヒフヘホマミムメモヤユヨラリルレロワヰヱヲン', cjkSuffix); case 29 /* KATAKANA_IROHA */: return createCounterStyleFromSymbols(value, 'イロハニホヘトチリヌルヲワカヨタレソツネナラムウヰノオクヤマケフコエテアサキユメミシヱヒモセス', cjkSuffix); case 34 /* LAO */: return createCounterStyleFromRange(value, 0xed0, 0xed9, true, defaultSuffix); case 37 /* MONGOLIAN */: return createCounterStyleFromRange(value, 0x1810, 0x1819, true, defaultSuffix); case 38 /* MYANMAR */: return createCounterStyleFromRange(value, 0x1040, 0x1049, true, defaultSuffix); case 39 /* ORIYA */: return createCounterStyleFromRange(value, 0xb66, 0xb6f, true, defaultSuffix); case 40 /* PERSIAN */: return createCounterStyleFromRange(value, 0x6f0, 0x6f9, true, defaultSuffix); case 43 /* TAMIL */: return createCounterStyleFromRange(value, 0xbe6, 0xbef, true, defaultSuffix); case 44 /* TELUGU */: return createCounterStyleFromRange(value, 0xc66, 0xc6f, true, defaultSuffix); case 45 /* THAI */: return createCounterStyleFromRange(value, 0xe50, 0xe59, true, defaultSuffix); case 46 /* TIBETAN */: return createCounterStyleFromRange(value, 0xf20, 0xf29, true, defaultSuffix); case 3 /* DECIMAL */: default: return createCounterStyleFromRange(value, 48, 57, true, defaultSuffix); } }; var IGNORE_ATTRIBUTE = 'data-html2canvas-ignore'; var DocumentCloner = /** @class */ (function () { function DocumentCloner(context, element, options) { this.context = context; this.options = options; this.scrolledElements = []; this.referenceElement = element; this.counters = new CounterState(); this.quoteDepth = 0; if (!element.ownerDocument) { throw new Error('Cloned element does not have an owner document'); } this.documentElement = this.cloneNode(element.ownerDocument.documentElement, false); } DocumentCloner.prototype.toIFrame = function (ownerDocument, windowSize) { var _this = this; var iframe = createIFrameContainer(ownerDocument, windowSize); if (!iframe.contentWindow) { return Promise.reject("Unable to find iframe window"); } var scrollX = ownerDocument.defaultView.pageXOffset; var scrollY = ownerDocument.defaultView.pageYOffset; var cloneWindow = iframe.contentWindow; var documentClone = cloneWindow.document; /* Chrome doesn't detect relative background-images assigned in inline